unreal-engine-mcp-server 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.dockerignore +57 -0
- package/.env.production +25 -0
- package/.eslintrc.json +54 -0
- package/.github/workflows/publish-mcp.yml +75 -0
- package/Dockerfile +54 -0
- package/LICENSE +21 -0
- package/Public/icon.png +0 -0
- package/README.md +209 -0
- package/claude_desktop_config_example.json +13 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.js +7 -0
- package/dist/index.d.ts +31 -0
- package/dist/index.js +484 -0
- package/dist/prompts/index.d.ts +14 -0
- package/dist/prompts/index.js +38 -0
- package/dist/python-utils.d.ts +29 -0
- package/dist/python-utils.js +54 -0
- package/dist/resources/actors.d.ts +13 -0
- package/dist/resources/actors.js +83 -0
- package/dist/resources/assets.d.ts +23 -0
- package/dist/resources/assets.js +245 -0
- package/dist/resources/levels.d.ts +17 -0
- package/dist/resources/levels.js +94 -0
- package/dist/tools/actors.d.ts +51 -0
- package/dist/tools/actors.js +459 -0
- package/dist/tools/animation.d.ts +196 -0
- package/dist/tools/animation.js +579 -0
- package/dist/tools/assets.d.ts +21 -0
- package/dist/tools/assets.js +304 -0
- package/dist/tools/audio.d.ts +170 -0
- package/dist/tools/audio.js +416 -0
- package/dist/tools/blueprint.d.ts +144 -0
- package/dist/tools/blueprint.js +652 -0
- package/dist/tools/build_environment_advanced.d.ts +66 -0
- package/dist/tools/build_environment_advanced.js +484 -0
- package/dist/tools/consolidated-tool-definitions.d.ts +2598 -0
- package/dist/tools/consolidated-tool-definitions.js +607 -0
- package/dist/tools/consolidated-tool-handlers.d.ts +2 -0
- package/dist/tools/consolidated-tool-handlers.js +1050 -0
- package/dist/tools/debug.d.ts +185 -0
- package/dist/tools/debug.js +265 -0
- package/dist/tools/editor.d.ts +88 -0
- package/dist/tools/editor.js +365 -0
- package/dist/tools/engine.d.ts +30 -0
- package/dist/tools/engine.js +36 -0
- package/dist/tools/foliage.d.ts +155 -0
- package/dist/tools/foliage.js +525 -0
- package/dist/tools/introspection.d.ts +98 -0
- package/dist/tools/introspection.js +683 -0
- package/dist/tools/landscape.d.ts +158 -0
- package/dist/tools/landscape.js +375 -0
- package/dist/tools/level.d.ts +110 -0
- package/dist/tools/level.js +362 -0
- package/dist/tools/lighting.d.ts +159 -0
- package/dist/tools/lighting.js +1179 -0
- package/dist/tools/materials.d.ts +34 -0
- package/dist/tools/materials.js +146 -0
- package/dist/tools/niagara.d.ts +145 -0
- package/dist/tools/niagara.js +289 -0
- package/dist/tools/performance.d.ts +163 -0
- package/dist/tools/performance.js +412 -0
- package/dist/tools/physics.d.ts +189 -0
- package/dist/tools/physics.js +784 -0
- package/dist/tools/rc.d.ts +110 -0
- package/dist/tools/rc.js +363 -0
- package/dist/tools/sequence.d.ts +112 -0
- package/dist/tools/sequence.js +675 -0
- package/dist/tools/tool-definitions.d.ts +4919 -0
- package/dist/tools/tool-definitions.js +891 -0
- package/dist/tools/tool-handlers.d.ts +47 -0
- package/dist/tools/tool-handlers.js +830 -0
- package/dist/tools/ui.d.ts +171 -0
- package/dist/tools/ui.js +337 -0
- package/dist/tools/visual.d.ts +29 -0
- package/dist/tools/visual.js +67 -0
- package/dist/types/env.d.ts +10 -0
- package/dist/types/env.js +18 -0
- package/dist/types/index.d.ts +323 -0
- package/dist/types/index.js +28 -0
- package/dist/types/tool-types.d.ts +274 -0
- package/dist/types/tool-types.js +13 -0
- package/dist/unreal-bridge.d.ts +126 -0
- package/dist/unreal-bridge.js +992 -0
- package/dist/utils/cache-manager.d.ts +64 -0
- package/dist/utils/cache-manager.js +176 -0
- package/dist/utils/error-handler.d.ts +66 -0
- package/dist/utils/error-handler.js +243 -0
- package/dist/utils/errors.d.ts +133 -0
- package/dist/utils/errors.js +256 -0
- package/dist/utils/http.d.ts +26 -0
- package/dist/utils/http.js +135 -0
- package/dist/utils/logger.d.ts +12 -0
- package/dist/utils/logger.js +32 -0
- package/dist/utils/normalize.d.ts +17 -0
- package/dist/utils/normalize.js +49 -0
- package/dist/utils/response-validator.d.ts +34 -0
- package/dist/utils/response-validator.js +121 -0
- package/dist/utils/safe-json.d.ts +4 -0
- package/dist/utils/safe-json.js +97 -0
- package/dist/utils/stdio-redirect.d.ts +2 -0
- package/dist/utils/stdio-redirect.js +20 -0
- package/dist/utils/validation.d.ts +50 -0
- package/dist/utils/validation.js +173 -0
- package/mcp-config-example.json +14 -0
- package/package.json +63 -0
- package/server.json +60 -0
- package/src/cli.ts +7 -0
- package/src/index.ts +543 -0
- package/src/prompts/index.ts +51 -0
- package/src/python/editor_compat.py +181 -0
- package/src/python-utils.ts +57 -0
- package/src/resources/actors.ts +92 -0
- package/src/resources/assets.ts +251 -0
- package/src/resources/levels.ts +83 -0
- package/src/tools/actors.ts +480 -0
- package/src/tools/animation.ts +713 -0
- package/src/tools/assets.ts +305 -0
- package/src/tools/audio.ts +548 -0
- package/src/tools/blueprint.ts +736 -0
- package/src/tools/build_environment_advanced.ts +526 -0
- package/src/tools/consolidated-tool-definitions.ts +619 -0
- package/src/tools/consolidated-tool-handlers.ts +1093 -0
- package/src/tools/debug.ts +368 -0
- package/src/tools/editor.ts +360 -0
- package/src/tools/engine.ts +32 -0
- package/src/tools/foliage.ts +652 -0
- package/src/tools/introspection.ts +778 -0
- package/src/tools/landscape.ts +523 -0
- package/src/tools/level.ts +410 -0
- package/src/tools/lighting.ts +1316 -0
- package/src/tools/materials.ts +148 -0
- package/src/tools/niagara.ts +312 -0
- package/src/tools/performance.ts +549 -0
- package/src/tools/physics.ts +924 -0
- package/src/tools/rc.ts +437 -0
- package/src/tools/sequence.ts +791 -0
- package/src/tools/tool-definitions.ts +907 -0
- package/src/tools/tool-handlers.ts +941 -0
- package/src/tools/ui.ts +499 -0
- package/src/tools/visual.ts +60 -0
- package/src/types/env.ts +27 -0
- package/src/types/index.ts +414 -0
- package/src/types/tool-types.ts +343 -0
- package/src/unreal-bridge.ts +1118 -0
- package/src/utils/cache-manager.ts +213 -0
- package/src/utils/error-handler.ts +320 -0
- package/src/utils/errors.ts +312 -0
- package/src/utils/http.ts +184 -0
- package/src/utils/logger.ts +30 -0
- package/src/utils/normalize.ts +54 -0
- package/src/utils/response-validator.ts +145 -0
- package/src/utils/safe-json.ts +112 -0
- package/src/utils/stdio-redirect.ts +18 -0
- package/src/utils/validation.ts +212 -0
- package/tsconfig.json +33 -0
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import Ajv from 'ajv';
|
|
2
|
+
import { Logger } from './logger.js';
|
|
3
|
+
import { cleanObject } from './safe-json.js';
|
|
4
|
+
|
|
5
|
+
const log = new Logger('ResponseValidator');
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Response Validator for MCP Tool Outputs
|
|
9
|
+
* Validates tool responses against their defined output schemas
|
|
10
|
+
*/
|
|
11
|
+
export class ResponseValidator {
|
|
12
|
+
private ajv: Ajv;
|
|
13
|
+
private validators: Map<string, any> = new Map();
|
|
14
|
+
|
|
15
|
+
constructor() {
|
|
16
|
+
this.ajv = new Ajv({
|
|
17
|
+
allErrors: true,
|
|
18
|
+
verbose: true,
|
|
19
|
+
strict: false // Allow additional properties for flexibility
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Register a tool's output schema for validation
|
|
25
|
+
*/
|
|
26
|
+
registerSchema(toolName: string, outputSchema: any) {
|
|
27
|
+
if (!outputSchema) {
|
|
28
|
+
log.warn(`No output schema defined for tool: ${toolName}`);
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
try {
|
|
33
|
+
const validator = this.ajv.compile(outputSchema);
|
|
34
|
+
this.validators.set(toolName, validator);
|
|
35
|
+
log.info(`Registered output schema for tool: ${toolName}`);
|
|
36
|
+
} catch (error) {
|
|
37
|
+
log.error(`Failed to compile output schema for ${toolName}:`, error);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Validate a tool's response against its schema
|
|
43
|
+
*/
|
|
44
|
+
validateResponse(toolName: string, response: any): {
|
|
45
|
+
valid: boolean;
|
|
46
|
+
errors?: string[];
|
|
47
|
+
structuredContent?: any;
|
|
48
|
+
} {
|
|
49
|
+
const validator = this.validators.get(toolName);
|
|
50
|
+
|
|
51
|
+
if (!validator) {
|
|
52
|
+
log.debug(`No validator found for tool: ${toolName}`);
|
|
53
|
+
return { valid: true }; // Pass through if no schema defined
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Extract structured content from response
|
|
57
|
+
let structuredContent = response;
|
|
58
|
+
|
|
59
|
+
// If response has MCP format with content array
|
|
60
|
+
if (response.content && Array.isArray(response.content)) {
|
|
61
|
+
// Try to extract structured data from text content
|
|
62
|
+
const textContent = response.content.find((c: any) => c.type === 'text');
|
|
63
|
+
if (textContent?.text) {
|
|
64
|
+
try {
|
|
65
|
+
// Check if text is JSON
|
|
66
|
+
structuredContent = JSON.parse(textContent.text);
|
|
67
|
+
} catch {
|
|
68
|
+
// Not JSON, use the full response
|
|
69
|
+
structuredContent = response;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const valid = validator(structuredContent);
|
|
75
|
+
|
|
76
|
+
if (!valid) {
|
|
77
|
+
const errors = validator.errors?.map((err: any) =>
|
|
78
|
+
`${err.instancePath || 'root'}: ${err.message}`
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
log.warn(`Response validation failed for ${toolName}:`, errors);
|
|
82
|
+
|
|
83
|
+
return {
|
|
84
|
+
valid: false,
|
|
85
|
+
errors,
|
|
86
|
+
structuredContent
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return {
|
|
91
|
+
valid: true,
|
|
92
|
+
structuredContent
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Wrap a tool response with validation
|
|
98
|
+
*/
|
|
99
|
+
wrapResponse(toolName: string, response: any): any {
|
|
100
|
+
// Ensure response is safe to serialize first
|
|
101
|
+
try {
|
|
102
|
+
// The response should already be cleaned, but double-check
|
|
103
|
+
if (response && typeof response === 'object') {
|
|
104
|
+
// Make sure we can serialize it
|
|
105
|
+
JSON.stringify(response);
|
|
106
|
+
}
|
|
107
|
+
} catch (error) {
|
|
108
|
+
log.error(`Response for ${toolName} contains circular references, cleaning...`);
|
|
109
|
+
response = cleanObject(response);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const validation = this.validateResponse(toolName, response);
|
|
113
|
+
|
|
114
|
+
// Add validation metadata
|
|
115
|
+
if (!validation.valid) {
|
|
116
|
+
log.warn(`Tool ${toolName} response validation failed:`, validation.errors);
|
|
117
|
+
|
|
118
|
+
// Add warning to response but don't fail
|
|
119
|
+
if (response && typeof response === 'object') {
|
|
120
|
+
response._validation = {
|
|
121
|
+
valid: false,
|
|
122
|
+
errors: validation.errors
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Don't add structuredContent to the response - it's for internal validation only
|
|
128
|
+
// Adding it can cause circular references
|
|
129
|
+
|
|
130
|
+
return response;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Get validation statistics
|
|
135
|
+
*/
|
|
136
|
+
getStats() {
|
|
137
|
+
return {
|
|
138
|
+
totalSchemas: this.validators.size,
|
|
139
|
+
tools: Array.from(this.validators.keys())
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Singleton instance
|
|
145
|
+
export const responseValidator = new ResponseValidator();
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
// Utility to safely serialize objects with circular references
|
|
2
|
+
export function safeStringify(obj: any, space?: number): string {
|
|
3
|
+
const seen = new WeakSet();
|
|
4
|
+
|
|
5
|
+
return JSON.stringify(obj, (key, value) => {
|
|
6
|
+
// Handle undefined, functions, symbols
|
|
7
|
+
if (value === undefined || typeof value === 'function' || typeof value === 'symbol') {
|
|
8
|
+
return undefined;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// Handle circular references
|
|
12
|
+
if (typeof value === 'object' && value !== null) {
|
|
13
|
+
if (seen.has(value)) {
|
|
14
|
+
return '[Circular Reference]';
|
|
15
|
+
}
|
|
16
|
+
seen.add(value);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Handle BigInt
|
|
20
|
+
if (typeof value === 'bigint') {
|
|
21
|
+
return value.toString();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return value;
|
|
25
|
+
}, space);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function sanitizeResponse(response: any): any {
|
|
29
|
+
if (!response || typeof response !== 'object') {
|
|
30
|
+
return response;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Create a clean copy without circular references
|
|
34
|
+
try {
|
|
35
|
+
const jsonStr = safeStringify(response);
|
|
36
|
+
return JSON.parse(jsonStr);
|
|
37
|
+
} catch (error) {
|
|
38
|
+
console.error('Failed to sanitize response:', error);
|
|
39
|
+
|
|
40
|
+
// Fallback: return a simple error response
|
|
41
|
+
return {
|
|
42
|
+
content: [{
|
|
43
|
+
type: 'text',
|
|
44
|
+
text: 'Response contained unserializable data'
|
|
45
|
+
}],
|
|
46
|
+
error: 'Serialization error'
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Remove circular references and non-serializable properties from an object
|
|
52
|
+
export function cleanObject(obj: any, maxDepth: number = 10): any {
|
|
53
|
+
const seen = new WeakSet();
|
|
54
|
+
|
|
55
|
+
function clean(value: any, depth: number, path: string = 'root'): any {
|
|
56
|
+
// Prevent infinite recursion
|
|
57
|
+
if (depth > maxDepth) {
|
|
58
|
+
return '[Max depth reached]';
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Handle primitives
|
|
62
|
+
if (value === null || value === undefined) {
|
|
63
|
+
return value;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (typeof value !== 'object') {
|
|
67
|
+
if (typeof value === 'function' || typeof value === 'symbol') {
|
|
68
|
+
return undefined;
|
|
69
|
+
}
|
|
70
|
+
if (typeof value === 'bigint') {
|
|
71
|
+
return value.toString();
|
|
72
|
+
}
|
|
73
|
+
return value;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Check for circular reference
|
|
77
|
+
if (seen.has(value)) {
|
|
78
|
+
return '[Circular Reference]';
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
seen.add(value);
|
|
82
|
+
|
|
83
|
+
// Handle arrays
|
|
84
|
+
if (Array.isArray(value)) {
|
|
85
|
+
const result = value.map((item, index) => clean(item, depth + 1, `${path}[${index}]`));
|
|
86
|
+
seen.delete(value); // Remove from seen after processing
|
|
87
|
+
return result;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Handle objects
|
|
91
|
+
const cleaned: any = {};
|
|
92
|
+
|
|
93
|
+
// Use Object.keys to avoid prototype properties
|
|
94
|
+
const keys = Object.keys(value);
|
|
95
|
+
for (const key of keys) {
|
|
96
|
+
try {
|
|
97
|
+
const cleanedValue = clean(value[key], depth + 1, `${path}.${key}`);
|
|
98
|
+
if (cleanedValue !== undefined) {
|
|
99
|
+
cleaned[key] = cleanedValue;
|
|
100
|
+
}
|
|
101
|
+
} catch (e) {
|
|
102
|
+
// Skip properties that throw errors when accessed
|
|
103
|
+
console.error(`Error cleaning property ${path}.${key}:`, e);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
seen.delete(value); // Remove from seen after processing
|
|
108
|
+
return cleaned;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return clean(obj, 0);
|
|
112
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export function routeStdoutLogsToStderr(): void {
|
|
2
|
+
// Enable by default. Allow opt-out with LOG_TO_STDERR=false or JSON_STDOUT_MODE=false
|
|
3
|
+
const flagRaw = String(process.env.LOG_TO_STDERR ?? process.env.JSON_STDOUT_MODE ?? 'true').toLowerCase();
|
|
4
|
+
const enabled = !(flagRaw === 'false' || flagRaw === '0' || flagRaw === 'off' || flagRaw === 'no');
|
|
5
|
+
if (!enabled) return;
|
|
6
|
+
|
|
7
|
+
try {
|
|
8
|
+
const toErr = console.error.bind(console) as (...args: any[]) => void;
|
|
9
|
+
// Route common stdout channels to stderr to keep stdout JSON-only for MCP
|
|
10
|
+
console.log = toErr as any;
|
|
11
|
+
console.info = toErr as any;
|
|
12
|
+
console.debug = toErr as any;
|
|
13
|
+
// Be explicit with trace as well
|
|
14
|
+
console.trace = toErr as any;
|
|
15
|
+
} catch {
|
|
16
|
+
// If overriding fails for any reason, just continue silently.
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Validation and sanitization utilities for Unreal Engine assets
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Maximum path length allowed in Unreal Engine
|
|
7
|
+
*/
|
|
8
|
+
const MAX_PATH_LENGTH = 260;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Invalid characters for Unreal Engine asset names
|
|
12
|
+
* Note: Dashes are allowed in Unreal asset names
|
|
13
|
+
*/
|
|
14
|
+
// eslint-disable-next-line no-useless-escape
|
|
15
|
+
const INVALID_CHARS = /[@#%$&*()+=\[\]{}<>?|\\;:'"`,~!\s]/g;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Reserved keywords that shouldn't be used as names
|
|
19
|
+
*/
|
|
20
|
+
const RESERVED_KEYWORDS = [
|
|
21
|
+
'None', 'null', 'undefined', 'true', 'false',
|
|
22
|
+
'class', 'struct', 'enum', 'interface',
|
|
23
|
+
'default', 'transient', 'native'
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Sanitize an asset name for Unreal Engine
|
|
28
|
+
* @param name The name to sanitize
|
|
29
|
+
* @returns Sanitized name
|
|
30
|
+
*/
|
|
31
|
+
export function sanitizeAssetName(name: string): string {
|
|
32
|
+
if (!name || typeof name !== 'string') {
|
|
33
|
+
return 'Asset';
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Remove leading/trailing whitespace
|
|
37
|
+
let sanitized = name.trim();
|
|
38
|
+
|
|
39
|
+
// Replace invalid characters with underscores
|
|
40
|
+
sanitized = sanitized.replace(INVALID_CHARS, '_');
|
|
41
|
+
|
|
42
|
+
// Remove consecutive underscores
|
|
43
|
+
sanitized = sanitized.replace(/_+/g, '_');
|
|
44
|
+
|
|
45
|
+
// Remove leading/trailing underscores
|
|
46
|
+
sanitized = sanitized.replace(/^_+|_+$/g, '');
|
|
47
|
+
|
|
48
|
+
// If name is empty after sanitization, use default
|
|
49
|
+
if (!sanitized) {
|
|
50
|
+
return 'Asset';
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// If name is a reserved keyword, append underscore
|
|
54
|
+
if (RESERVED_KEYWORDS.includes(sanitized)) {
|
|
55
|
+
sanitized = `${sanitized}_Asset`;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Ensure name starts with a letter
|
|
59
|
+
if (!/^[A-Za-z]/.test(sanitized)) {
|
|
60
|
+
sanitized = `Asset_${sanitized}`;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Truncate overly long names to reduce risk of hitting path length limits
|
|
64
|
+
if (sanitized.length > 64) {
|
|
65
|
+
sanitized = sanitized.slice(0, 64);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return sanitized;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Sanitize a path for Unreal Engine
|
|
73
|
+
* @param path The path to sanitize
|
|
74
|
+
* @returns Sanitized path
|
|
75
|
+
*/
|
|
76
|
+
export function sanitizePath(path: string): string {
|
|
77
|
+
if (!path || typeof path !== 'string') {
|
|
78
|
+
return '/Game';
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Ensure path starts with /
|
|
82
|
+
if (!path.startsWith('/')) {
|
|
83
|
+
path = `/${path}`;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Split path into segments and sanitize each
|
|
87
|
+
const segments = path.split('/').filter(s => s.length > 0);
|
|
88
|
+
const sanitizedSegments = segments.map(segment => {
|
|
89
|
+
// Don't sanitize Game, Engine, or other root folders
|
|
90
|
+
if (['Game', 'Engine', 'Script', 'Temp'].includes(segment)) {
|
|
91
|
+
return segment;
|
|
92
|
+
}
|
|
93
|
+
return sanitizeAssetName(segment);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
// Reconstruct path
|
|
97
|
+
const sanitizedPath = '/' + sanitizedSegments.join('/');
|
|
98
|
+
|
|
99
|
+
return sanitizedPath;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Validate path length
|
|
104
|
+
* @param path The full path to validate
|
|
105
|
+
* @returns Object with validation result
|
|
106
|
+
*/
|
|
107
|
+
export function validatePathLength(path: string): { valid: boolean; error?: string } {
|
|
108
|
+
if (path.length > MAX_PATH_LENGTH) {
|
|
109
|
+
return {
|
|
110
|
+
valid: false,
|
|
111
|
+
error: `Path too long (${path.length} characters). Maximum allowed is ${MAX_PATH_LENGTH} characters.`
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
return { valid: true };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Validate and sanitize asset parameters
|
|
119
|
+
* @param params Object containing name and optionally savePath
|
|
120
|
+
* @returns Sanitized parameters with validation result
|
|
121
|
+
*/
|
|
122
|
+
export function validateAssetParams(params: {
|
|
123
|
+
name: string;
|
|
124
|
+
savePath?: string;
|
|
125
|
+
[key: string]: any;
|
|
126
|
+
}): {
|
|
127
|
+
valid: boolean;
|
|
128
|
+
sanitized: typeof params;
|
|
129
|
+
error?: string;
|
|
130
|
+
} {
|
|
131
|
+
// Sanitize name
|
|
132
|
+
const sanitizedName = sanitizeAssetName(params.name);
|
|
133
|
+
|
|
134
|
+
// Sanitize path if provided
|
|
135
|
+
const sanitizedPath = params.savePath
|
|
136
|
+
? sanitizePath(params.savePath)
|
|
137
|
+
: params.savePath;
|
|
138
|
+
|
|
139
|
+
// Construct full path for validation
|
|
140
|
+
const fullPath = sanitizedPath
|
|
141
|
+
? `${sanitizedPath}/${sanitizedName}`
|
|
142
|
+
: `/Game/${sanitizedName}`;
|
|
143
|
+
|
|
144
|
+
// Validate path length
|
|
145
|
+
const pathValidation = validatePathLength(fullPath);
|
|
146
|
+
|
|
147
|
+
if (!pathValidation.valid) {
|
|
148
|
+
return {
|
|
149
|
+
valid: false,
|
|
150
|
+
sanitized: params,
|
|
151
|
+
error: pathValidation.error
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return {
|
|
156
|
+
valid: true,
|
|
157
|
+
sanitized: {
|
|
158
|
+
...params,
|
|
159
|
+
name: sanitizedName,
|
|
160
|
+
...(sanitizedPath && { savePath: sanitizedPath })
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Extract valid skeletal mesh path from various inputs
|
|
167
|
+
* @param input The input path which might be a skeleton or mesh
|
|
168
|
+
* @returns Corrected skeletal mesh path or null
|
|
169
|
+
*/
|
|
170
|
+
export function resolveSkeletalMeshPath(input: string): string | null {
|
|
171
|
+
if (!input || typeof input !== 'string') {
|
|
172
|
+
return null;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Common skeleton to mesh mappings
|
|
176
|
+
const skeletonToMeshMap: { [key: string]: string } = {
|
|
177
|
+
'/Game/Mannequin/Character/Mesh/UE4_Mannequin_Skeleton': '/Game/Characters/Mannequins/Meshes/SKM_Manny',
|
|
178
|
+
'/Game/Characters/Mannequins/Meshes/SK_Mannequin': '/Game/Characters/Mannequins/Meshes/SKM_Manny',
|
|
179
|
+
'/Game/Mannequin/Character/Mesh/SK_Mannequin': '/Game/Characters/Mannequins/Meshes/SKM_Manny',
|
|
180
|
+
'/Game/Characters/Mannequin_UE4/Meshes/UE4_Mannequin_Skeleton': '/Game/Characters/Mannequins/Meshes/SKM_Quinn',
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
// Check if this is a known skeleton path
|
|
184
|
+
if (skeletonToMeshMap[input]) {
|
|
185
|
+
return skeletonToMeshMap[input];
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// If it contains _Skeleton, try to convert to mesh name
|
|
189
|
+
if (input.includes('_Skeleton')) {
|
|
190
|
+
// Try common replacements
|
|
191
|
+
let meshPath = input.replace('_Skeleton', '');
|
|
192
|
+
meshPath = meshPath.replace('/SK_', '/SKM_');
|
|
193
|
+
meshPath = meshPath.replace('UE4_Mannequin', 'SKM_Manny');
|
|
194
|
+
return meshPath;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// If it starts with SK_ (skeleton prefix), try SKM_ (skeletal mesh prefix)
|
|
198
|
+
if (input.includes('/SK_')) {
|
|
199
|
+
return input.replace('/SK_', '/SKM_');
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Return as-is if no conversion needed
|
|
203
|
+
return input;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Concurrency delay to prevent race conditions
|
|
208
|
+
* @param ms Milliseconds to delay
|
|
209
|
+
*/
|
|
210
|
+
export async function concurrencyDelay(ms: number = 100): Promise<void> {
|
|
211
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
212
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "ES2022",
|
|
5
|
+
"moduleResolution": "Bundler",
|
|
6
|
+
"outDir": "dist",
|
|
7
|
+
"rootDir": "src",
|
|
8
|
+
"strict": true,
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"forceConsistentCasingInFileNames": true,
|
|
11
|
+
"skipLibCheck": true,
|
|
12
|
+
"declaration": true,
|
|
13
|
+
"declarationMap": true,
|
|
14
|
+
"sourceMap": true,
|
|
15
|
+
"resolveJsonModule": true,
|
|
16
|
+
"incremental": true,
|
|
17
|
+
"tsBuildInfoFile": ".tsbuildinfo",
|
|
18
|
+
"noUnusedLocals": false,
|
|
19
|
+
"noUnusedParameters": false,
|
|
20
|
+
"noImplicitReturns": true,
|
|
21
|
+
"noFallthroughCasesInSwitch": true,
|
|
22
|
+
"allowUnreachableCode": false,
|
|
23
|
+
"types": ["node"]
|
|
24
|
+
},
|
|
25
|
+
"include": ["src/**/*"],
|
|
26
|
+
"exclude": [
|
|
27
|
+
"node_modules",
|
|
28
|
+
"dist",
|
|
29
|
+
"**/*.test.ts",
|
|
30
|
+
"**/*.spec.ts",
|
|
31
|
+
"src/tests/**/*"
|
|
32
|
+
]
|
|
33
|
+
}
|