unreal-engine-mcp-server 0.5.0 → 0.5.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.
Files changed (139) hide show
  1. package/.env.example +1 -1
  2. package/.github/release-drafter-config.yml +51 -0
  3. package/.github/workflows/greetings.yml +5 -1
  4. package/.github/workflows/labeler.yml +2 -1
  5. package/.github/workflows/publish-mcp.yml +1 -0
  6. package/.github/workflows/release-drafter.yml +1 -1
  7. package/.github/workflows/release.yml +3 -3
  8. package/CHANGELOG.md +71 -0
  9. package/CONTRIBUTING.md +1 -1
  10. package/GEMINI.md +115 -0
  11. package/Public/Plugin_setup_guide.mp4 +0 -0
  12. package/README.md +166 -200
  13. package/dist/config.d.ts +0 -1
  14. package/dist/config.js +0 -1
  15. package/dist/constants.d.ts +4 -0
  16. package/dist/constants.js +4 -0
  17. package/dist/graphql/loaders.d.ts +64 -0
  18. package/dist/graphql/loaders.js +117 -0
  19. package/dist/graphql/resolvers.d.ts +3 -3
  20. package/dist/graphql/resolvers.js +33 -30
  21. package/dist/graphql/server.js +3 -1
  22. package/dist/graphql/types.d.ts +2 -0
  23. package/dist/index.d.ts +2 -0
  24. package/dist/index.js +13 -2
  25. package/dist/server-setup.d.ts +0 -1
  26. package/dist/server-setup.js +0 -40
  27. package/dist/tools/actors.d.ts +40 -24
  28. package/dist/tools/actors.js +8 -2
  29. package/dist/tools/assets.d.ts +19 -71
  30. package/dist/tools/assets.js +28 -22
  31. package/dist/tools/base-tool.d.ts +4 -4
  32. package/dist/tools/base-tool.js +1 -1
  33. package/dist/tools/blueprint.d.ts +33 -61
  34. package/dist/tools/consolidated-tool-handlers.js +96 -110
  35. package/dist/tools/dynamic-handler-registry.d.ts +11 -9
  36. package/dist/tools/dynamic-handler-registry.js +17 -95
  37. package/dist/tools/editor.d.ts +19 -193
  38. package/dist/tools/editor.js +8 -0
  39. package/dist/tools/environment.d.ts +8 -14
  40. package/dist/tools/foliage.d.ts +18 -143
  41. package/dist/tools/foliage.js +4 -2
  42. package/dist/tools/handlers/actor-handlers.js +0 -5
  43. package/dist/tools/handlers/asset-handlers.js +454 -454
  44. package/dist/tools/landscape.d.ts +16 -116
  45. package/dist/tools/landscape.js +7 -3
  46. package/dist/tools/level.d.ts +22 -103
  47. package/dist/tools/level.js +24 -16
  48. package/dist/tools/lighting.js +5 -1
  49. package/dist/tools/materials.js +5 -1
  50. package/dist/tools/niagara.js +37 -2
  51. package/dist/tools/performance.d.ts +0 -1
  52. package/dist/tools/performance.js +0 -1
  53. package/dist/tools/physics.js +5 -1
  54. package/dist/tools/sequence.d.ts +24 -24
  55. package/dist/tools/sequence.js +13 -0
  56. package/dist/tools/ui.d.ts +0 -2
  57. package/dist/types/automation-responses.d.ts +115 -0
  58. package/dist/types/automation-responses.js +2 -0
  59. package/dist/types/responses.d.ts +249 -0
  60. package/dist/types/responses.js +2 -0
  61. package/dist/types/tool-interfaces.d.ts +135 -135
  62. package/dist/utils/command-validator.js +3 -2
  63. package/dist/utils/path-security.d.ts +2 -0
  64. package/dist/utils/path-security.js +24 -0
  65. package/dist/utils/response-factory.d.ts +4 -4
  66. package/dist/utils/response-factory.js +15 -21
  67. package/docs/Migration-Guide-v0.5.0.md +1 -9
  68. package/docs/testing-guide.md +2 -2
  69. package/package.json +12 -6
  70. package/scripts/run-all-tests.mjs +25 -20
  71. package/server.json +3 -2
  72. package/src/config.ts +1 -1
  73. package/src/constants.ts +7 -0
  74. package/src/graphql/loaders.ts +244 -0
  75. package/src/graphql/resolvers.ts +47 -49
  76. package/src/graphql/server.ts +3 -1
  77. package/src/graphql/types.ts +3 -0
  78. package/src/index.ts +15 -2
  79. package/src/resources/assets.ts +5 -4
  80. package/src/server-setup.ts +3 -37
  81. package/src/tools/actors.ts +36 -28
  82. package/src/tools/animation.ts +1 -0
  83. package/src/tools/assets.ts +74 -63
  84. package/src/tools/base-tool.ts +3 -3
  85. package/src/tools/blueprint.ts +59 -59
  86. package/src/tools/consolidated-tool-handlers.ts +129 -150
  87. package/src/tools/dynamic-handler-registry.ts +22 -140
  88. package/src/tools/editor.ts +39 -26
  89. package/src/tools/environment.ts +21 -27
  90. package/src/tools/foliage.ts +28 -25
  91. package/src/tools/handlers/actor-handlers.ts +2 -8
  92. package/src/tools/handlers/asset-handlers.ts +484 -484
  93. package/src/tools/handlers/sequence-handlers.ts +1 -1
  94. package/src/tools/landscape.ts +34 -28
  95. package/src/tools/level.ts +96 -76
  96. package/src/tools/lighting.ts +6 -1
  97. package/src/tools/materials.ts +8 -2
  98. package/src/tools/niagara.ts +44 -2
  99. package/src/tools/performance.ts +1 -2
  100. package/src/tools/physics.ts +7 -1
  101. package/src/tools/sequence.ts +41 -25
  102. package/src/tools/ui.ts +0 -2
  103. package/src/types/automation-responses.ts +119 -0
  104. package/src/types/responses.ts +355 -0
  105. package/src/types/tool-interfaces.ts +135 -135
  106. package/src/utils/command-validator.ts +3 -2
  107. package/src/utils/normalize.test.ts +162 -0
  108. package/src/utils/path-security.ts +43 -0
  109. package/src/utils/response-factory.ts +29 -24
  110. package/src/utils/safe-json.test.ts +90 -0
  111. package/src/utils/validation.test.ts +184 -0
  112. package/tests/test-animation.mjs +358 -33
  113. package/tests/test-asset-graph.mjs +311 -0
  114. package/tests/test-audio.mjs +314 -116
  115. package/tests/test-behavior-tree.mjs +327 -144
  116. package/tests/test-blueprint-graph.mjs +343 -12
  117. package/tests/test-control-editor.mjs +85 -53
  118. package/tests/test-graphql.mjs +58 -8
  119. package/tests/test-input.mjs +349 -0
  120. package/tests/test-inspect.mjs +291 -61
  121. package/tests/test-landscape.mjs +304 -48
  122. package/tests/test-lighting.mjs +428 -0
  123. package/tests/test-manage-level.mjs +70 -51
  124. package/tests/test-performance.mjs +539 -0
  125. package/tests/test-sequence.mjs +82 -46
  126. package/tests/test-system.mjs +72 -33
  127. package/tests/test-wasm.mjs +98 -8
  128. package/vitest.config.ts +35 -0
  129. package/.github/release-drafter.yml +0 -148
  130. package/dist/prompts/index.d.ts +0 -21
  131. package/dist/prompts/index.js +0 -217
  132. package/dist/tools/blueprint/helpers.d.ts +0 -29
  133. package/dist/tools/blueprint/helpers.js +0 -182
  134. package/src/prompts/index.ts +0 -249
  135. package/src/tools/blueprint/helpers.ts +0 -189
  136. package/tests/test-blueprint-events.mjs +0 -35
  137. package/tests/test-extra-tools.mjs +0 -38
  138. package/tests/test-render.mjs +0 -33
  139. package/tests/test-search-assets.mjs +0 -66
@@ -8,8 +8,8 @@ export interface StandardActionResponse<T = any> {
8
8
  success: boolean;
9
9
  data?: T;
10
10
  warnings?: string[];
11
- error?: {
12
- code: string;
11
+ error?: string | {
12
+ code?: string;
13
13
  message: string;
14
14
  [key: string]: unknown;
15
15
  } | null;
@@ -53,48 +53,48 @@ export interface SourceControlState {
53
53
  }
54
54
 
55
55
  export interface IAssetTools {
56
- importAsset(params: { sourcePath: string; destinationPath: string; overwrite?: boolean; save?: boolean }): Promise<any>;
57
- createFolder(path: string): Promise<any>;
58
- duplicateAsset(params: { sourcePath: string; destinationPath: string; overwrite?: boolean }): Promise<any>;
59
- renameAsset(params: { sourcePath: string; destinationPath: string }): Promise<any>;
60
- moveAsset(params: { sourcePath: string; destinationPath: string }): Promise<any>;
61
- deleteAssets(params: { paths: string[]; fixupRedirectors?: boolean; timeoutMs?: number }): Promise<any>;
62
- searchAssets(params: { classNames?: string[]; packagePaths?: string[]; recursivePaths?: boolean; recursiveClasses?: boolean; limit?: number }): Promise<any>;
63
- saveAsset(assetPath: string): Promise<any>;
64
- findByTag(params: { tag: string; value?: string }): Promise<any>;
65
- getDependencies(params: { assetPath: string; recursive?: boolean }): Promise<any>;
66
- getMetadata(params: { assetPath: string }): Promise<any>;
67
- getSourceControlState(params: { assetPath: string }): Promise<SourceControlState | any>;
68
- analyzeGraph(params: { assetPath: string; maxDepth?: number }): Promise<any>;
69
- createThumbnail(params: { assetPath: string; width?: number; height?: number }): Promise<any>;
70
- setTags(params: { assetPath: string; tags: string[] }): Promise<any>;
71
- generateReport(params: { directory: string; reportType?: string; outputPath?: string }): Promise<any>;
72
- validate(params: { assetPath: string }): Promise<any>;
73
- generateLODs(params: { assetPath: string; lodCount: number; reductionSettings?: Record<string, unknown> }): Promise<any>;
56
+ importAsset(params: { sourcePath: string; destinationPath: string; overwrite?: boolean; save?: boolean }): Promise<StandardActionResponse>;
57
+ createFolder(path: string): Promise<StandardActionResponse>;
58
+ duplicateAsset(params: { sourcePath: string; destinationPath: string; overwrite?: boolean }): Promise<StandardActionResponse>;
59
+ renameAsset(params: { sourcePath: string; destinationPath: string }): Promise<StandardActionResponse>;
60
+ moveAsset(params: { sourcePath: string; destinationPath: string }): Promise<StandardActionResponse>;
61
+ deleteAssets(params: { paths: string[]; fixupRedirectors?: boolean; timeoutMs?: number }): Promise<StandardActionResponse>;
62
+ searchAssets(params: { classNames?: string[]; packagePaths?: string[]; recursivePaths?: boolean; recursiveClasses?: boolean; limit?: number }): Promise<StandardActionResponse>;
63
+ saveAsset(assetPath: string): Promise<StandardActionResponse>;
64
+ findByTag(params: { tag: string; value?: string }): Promise<StandardActionResponse>;
65
+ getDependencies(params: { assetPath: string; recursive?: boolean }): Promise<StandardActionResponse>;
66
+ getMetadata(params: { assetPath: string }): Promise<StandardActionResponse>;
67
+ getSourceControlState(params: { assetPath: string }): Promise<SourceControlState | StandardActionResponse>;
68
+ analyzeGraph(params: { assetPath: string; maxDepth?: number }): Promise<StandardActionResponse>;
69
+ createThumbnail(params: { assetPath: string; width?: number; height?: number }): Promise<StandardActionResponse>;
70
+ setTags(params: { assetPath: string; tags: string[] }): Promise<StandardActionResponse>;
71
+ generateReport(params: { directory: string; reportType?: string; outputPath?: string }): Promise<StandardActionResponse>;
72
+ validate(params: { assetPath: string }): Promise<StandardActionResponse>;
73
+ generateLODs(params: { assetPath: string; lodCount: number; reductionSettings?: Record<string, unknown> }): Promise<StandardActionResponse>;
74
74
  }
75
75
 
76
76
  export interface ISequenceTools {
77
- create(params: { name: string; path?: string; timeoutMs?: number }): Promise<any>;
78
- open(params: { path: string }): Promise<any>;
79
- addCamera(params: { spawnable?: boolean; path?: string }): Promise<any>;
80
- addActor(params: { actorName: string; createBinding?: boolean; path?: string }): Promise<any>;
81
- addActors(params: { actorNames: string[]; path?: string }): Promise<any>;
82
- removeActors(params: { actorNames: string[]; path?: string }): Promise<any>;
83
- getBindings(params: { path?: string }): Promise<any>;
84
- addSpawnableFromClass(params: { className: string; path?: string }): Promise<any>;
85
- play(params: { path?: string; startTime?: number; loopMode?: 'once' | 'loop' | 'pingpong' }): Promise<any>;
86
- pause(params?: { path?: string }): Promise<any>;
87
- stop(params?: { path?: string }): Promise<any>;
88
- setSequenceProperties(params: { path?: string; frameRate?: number; lengthInFrames?: number; playbackStart?: number; playbackEnd?: number }): Promise<any>;
89
- getSequenceProperties(params: { path?: string }): Promise<any>;
90
- setPlaybackSpeed(params: { speed: number; path?: string }): Promise<any>;
91
- list(params: { path?: string }): Promise<any>;
92
- duplicate(params: { path: string; destinationPath: string }): Promise<any>;
93
- rename(params: { path: string; newName: string }): Promise<any>;
94
- deleteSequence(params: { path: string }): Promise<any>;
95
- getMetadata(params: { path: string }): Promise<any>;
96
- listTracks(params: { path: string }): Promise<any>;
97
- setWorkRange(params: { path?: string; start: number; end: number }): Promise<any>;
77
+ create(params: { name: string; path?: string; timeoutMs?: number }): Promise<StandardActionResponse>;
78
+ open(params: { path: string }): Promise<StandardActionResponse>;
79
+ addCamera(params: { spawnable?: boolean; path?: string }): Promise<StandardActionResponse>;
80
+ addActor(params: { actorName: string; createBinding?: boolean; path?: string }): Promise<StandardActionResponse>;
81
+ addActors(params: { actorNames: string[]; path?: string }): Promise<StandardActionResponse>;
82
+ removeActors(params: { actorNames: string[]; path?: string }): Promise<StandardActionResponse>;
83
+ getBindings(params: { path?: string }): Promise<StandardActionResponse>;
84
+ addSpawnableFromClass(params: { className: string; path?: string }): Promise<StandardActionResponse>;
85
+ play(params: { path?: string; startTime?: number; loopMode?: 'once' | 'loop' | 'pingpong' }): Promise<StandardActionResponse>;
86
+ pause(params?: { path?: string }): Promise<StandardActionResponse>;
87
+ stop(params?: { path?: string }): Promise<StandardActionResponse>;
88
+ setSequenceProperties(params: { path?: string; frameRate?: number; lengthInFrames?: number; playbackStart?: number; playbackEnd?: number }): Promise<StandardActionResponse>;
89
+ getSequenceProperties(params: { path?: string }): Promise<StandardActionResponse>;
90
+ setPlaybackSpeed(params: { speed: number; path?: string }): Promise<StandardActionResponse>;
91
+ list(params: { path?: string }): Promise<StandardActionResponse>;
92
+ duplicate(params: { path: string; destinationPath: string }): Promise<StandardActionResponse>;
93
+ rename(params: { path: string; newName: string }): Promise<StandardActionResponse>;
94
+ deleteSequence(params: { path: string }): Promise<StandardActionResponse>;
95
+ getMetadata(params: { path: string }): Promise<StandardActionResponse>;
96
+ listTracks(params: { path: string }): Promise<StandardActionResponse>;
97
+ setWorkRange(params: { path?: string; start: number; end: number }): Promise<StandardActionResponse>;
98
98
  }
99
99
 
100
100
  export interface IAssetResources {
@@ -102,118 +102,118 @@ export interface IAssetResources {
102
102
  }
103
103
 
104
104
  export interface IBlueprintTools {
105
- createBlueprint(params: { name: string; blueprintType?: string; savePath?: string; parentClass?: string; properties?: Record<string, unknown>; timeoutMs?: number; waitForCompletion?: boolean; waitForCompletionTimeoutMs?: number }): Promise<any>;
106
- modifyConstructionScript(params: { blueprintPath: string; operations: any[]; compile?: boolean; save?: boolean; timeoutMs?: number; waitForCompletion?: boolean; waitForCompletionTimeoutMs?: number }): Promise<any>;
107
- addComponent(params: { blueprintName: string; componentType: string; componentName: string; attachTo?: string; transform?: Record<string, unknown>; properties?: Record<string, unknown>; compile?: boolean; save?: boolean; timeoutMs?: number; waitForCompletion?: boolean; waitForCompletionTimeoutMs?: number }): Promise<any>;
108
- waitForBlueprint(blueprintRef: string | string[], timeoutMs?: number): Promise<any>;
109
- getBlueprint(params: { blueprintName: string; timeoutMs?: number }): Promise<any>;
110
- getBlueprintInfo(params: { blueprintPath: string; timeoutMs?: number }): Promise<any>;
111
- probeSubobjectDataHandle(opts?: { componentClass?: string }): Promise<any>;
112
- setBlueprintDefault(params: { blueprintName: string; propertyName: string; value: unknown }): Promise<any>;
113
- addVariable(params: { blueprintName: string; variableName: string; variableType: string; defaultValue?: any; category?: string; isReplicated?: boolean; isPublic?: boolean; variablePinType?: Record<string, unknown>; timeoutMs?: number; waitForCompletion?: boolean; waitForCompletionTimeoutMs?: number }): Promise<any>;
114
- removeVariable(params: { blueprintName: string; variableName: string; timeoutMs?: number; waitForCompletion?: boolean; waitForCompletionTimeoutMs?: number }): Promise<any>;
115
- renameVariable(params: { blueprintName: string; oldName: string; newName: string; timeoutMs?: number; waitForCompletion?: boolean; waitForCompletionTimeoutMs?: number }): Promise<any>;
116
- addEvent(params: { blueprintName: string; eventType: string; customEventName?: string; parameters?: Array<{ name: string; type: string }>; timeoutMs?: number; waitForCompletion?: boolean; waitForCompletionTimeoutMs?: number }): Promise<any>;
117
- removeEvent(params: { blueprintName: string; eventName: string; customEventName?: string; timeoutMs?: number; waitForCompletion?: boolean; waitForCompletionTimeoutMs?: number }): Promise<any>;
118
- addFunction(params: { blueprintName: string; functionName: string; inputs?: Array<{ name: string; type: string }>; outputs?: Array<{ name: string; type: string }>; isPublic?: boolean; category?: string; timeoutMs?: number; waitForCompletion?: boolean; waitForCompletionTimeoutMs?: number }): Promise<any>;
119
- setVariableMetadata(params: { blueprintName: string; variableName: string; metadata: Record<string, unknown>; timeoutMs?: number }): Promise<any>;
120
- renameVariable(params: { blueprintName: string; oldName: string; newName: string; timeoutMs?: number; waitForCompletion?: boolean; waitForCompletionTimeoutMs?: number }): Promise<any>;
121
- addConstructionScript(params: { blueprintName: string; scriptName: string; timeoutMs?: number; waitForCompletion?: boolean; waitForCompletionTimeoutMs?: number }): Promise<any>;
122
- compileBlueprint(params: { blueprintName: string; saveAfterCompile?: boolean }): Promise<any>;
123
- getBlueprintSCS(params: { blueprintPath: string; timeoutMs?: number }): Promise<any>;
124
- addSCSComponent(params: { blueprintPath: string; componentClass: string; componentName: string; parentComponent?: string; meshPath?: string; materialPath?: string; timeoutMs?: number }): Promise<any>;
125
- removeSCSComponent(params: { blueprintPath: string; componentName: string; timeoutMs?: number }): Promise<any>;
126
- reparentSCSComponent(params: { blueprintPath: string; componentName: string; newParent: string; timeoutMs?: number }): Promise<any>;
127
- setSCSComponentTransform(params: { blueprintPath: string; componentName: string; location?: [number, number, number]; rotation?: [number, number, number]; scale?: [number, number, number]; timeoutMs?: number }): Promise<any>;
128
- setSCSComponentProperty(params: { blueprintPath: string; componentName: string; propertyName: string; propertyValue: any; timeoutMs?: number }): Promise<any>;
129
- addNode(params: { blueprintName: string; nodeType: string; graphName?: string; functionName?: string; variableName?: string; nodeName?: string; eventName?: string; memberClass?: string; posX?: number; posY?: number; timeoutMs?: number }): Promise<any>;
130
- connectPins(params: { blueprintName: string; sourceNodeGuid: string; targetNodeGuid: string; sourcePinName?: string; targetPinName?: string; timeoutMs?: number }): Promise<any>;
105
+ createBlueprint(params: { name: string; blueprintType?: string; savePath?: string; parentClass?: string; properties?: Record<string, unknown>; timeoutMs?: number; waitForCompletion?: boolean; waitForCompletionTimeoutMs?: number }): Promise<StandardActionResponse>;
106
+ modifyConstructionScript(params: { blueprintPath: string; operations: any[]; compile?: boolean; save?: boolean; timeoutMs?: number; waitForCompletion?: boolean; waitForCompletionTimeoutMs?: number }): Promise<StandardActionResponse>;
107
+ addComponent(params: { blueprintName: string; componentType: string; componentName: string; attachTo?: string; transform?: Record<string, unknown>; properties?: Record<string, unknown>; compile?: boolean; save?: boolean; timeoutMs?: number; waitForCompletion?: boolean; waitForCompletionTimeoutMs?: number }): Promise<StandardActionResponse>;
108
+ waitForBlueprint(blueprintRef: string | string[], timeoutMs?: number): Promise<StandardActionResponse>;
109
+ getBlueprint(params: { blueprintName: string; timeoutMs?: number }): Promise<StandardActionResponse>;
110
+ getBlueprintInfo(params: { blueprintPath: string; timeoutMs?: number }): Promise<StandardActionResponse>;
111
+ probeSubobjectDataHandle(opts?: { componentClass?: string }): Promise<StandardActionResponse>;
112
+ setBlueprintDefault(params: { blueprintName: string; propertyName: string; value: unknown }): Promise<StandardActionResponse>;
113
+ addVariable(params: { blueprintName: string; variableName: string; variableType: string; defaultValue?: any; category?: string; isReplicated?: boolean; isPublic?: boolean; variablePinType?: Record<string, unknown>; timeoutMs?: number; waitForCompletion?: boolean; waitForCompletionTimeoutMs?: number }): Promise<StandardActionResponse>;
114
+ removeVariable(params: { blueprintName: string; variableName: string; timeoutMs?: number; waitForCompletion?: boolean; waitForCompletionTimeoutMs?: number }): Promise<StandardActionResponse>;
115
+ renameVariable(params: { blueprintName: string; oldName: string; newName: string; timeoutMs?: number; waitForCompletion?: boolean; waitForCompletionTimeoutMs?: number }): Promise<StandardActionResponse>;
116
+ addEvent(params: { blueprintName: string; eventType: string; customEventName?: string; parameters?: Array<{ name: string; type: string }>; timeoutMs?: number; waitForCompletion?: boolean; waitForCompletionTimeoutMs?: number }): Promise<StandardActionResponse>;
117
+ removeEvent(params: { blueprintName: string; eventName: string; customEventName?: string; timeoutMs?: number; waitForCompletion?: boolean; waitForCompletionTimeoutMs?: number }): Promise<StandardActionResponse>;
118
+ addFunction(params: { blueprintName: string; functionName: string; inputs?: Array<{ name: string; type: string }>; outputs?: Array<{ name: string; type: string }>; isPublic?: boolean; category?: string; timeoutMs?: number; waitForCompletion?: boolean; waitForCompletionTimeoutMs?: number }): Promise<StandardActionResponse>;
119
+ setVariableMetadata(params: { blueprintName: string; variableName: string; metadata: Record<string, unknown>; timeoutMs?: number }): Promise<StandardActionResponse>;
120
+ renameVariable(params: { blueprintName: string; oldName: string; newName: string; timeoutMs?: number; waitForCompletion?: boolean; waitForCompletionTimeoutMs?: number }): Promise<StandardActionResponse>;
121
+ addConstructionScript(params: { blueprintName: string; scriptName: string; timeoutMs?: number; waitForCompletion?: boolean; waitForCompletionTimeoutMs?: number }): Promise<StandardActionResponse>;
122
+ compileBlueprint(params: { blueprintName: string; saveAfterCompile?: boolean }): Promise<StandardActionResponse>;
123
+ getBlueprintSCS(params: { blueprintPath: string; timeoutMs?: number }): Promise<StandardActionResponse>;
124
+ addSCSComponent(params: { blueprintPath: string; componentClass: string; componentName: string; parentComponent?: string; meshPath?: string; materialPath?: string; timeoutMs?: number }): Promise<StandardActionResponse>;
125
+ removeSCSComponent(params: { blueprintPath: string; componentName: string; timeoutMs?: number }): Promise<StandardActionResponse>;
126
+ reparentSCSComponent(params: { blueprintPath: string; componentName: string; newParent: string; timeoutMs?: number }): Promise<StandardActionResponse>;
127
+ setSCSComponentTransform(params: { blueprintPath: string; componentName: string; location?: [number, number, number]; rotation?: [number, number, number]; scale?: [number, number, number]; timeoutMs?: number }): Promise<StandardActionResponse>;
128
+ setSCSComponentProperty(params: { blueprintPath: string; componentName: string; propertyName: string; propertyValue: any; timeoutMs?: number }): Promise<StandardActionResponse>;
129
+ addNode(params: { blueprintName: string; nodeType: string; graphName?: string; functionName?: string; variableName?: string; nodeName?: string; eventName?: string; memberClass?: string; posX?: number; posY?: number; timeoutMs?: number }): Promise<StandardActionResponse>;
130
+ connectPins(params: { blueprintName: string; sourceNodeGuid: string; targetNodeGuid: string; sourcePinName?: string; targetPinName?: string; timeoutMs?: number }): Promise<StandardActionResponse>;
131
131
  }
132
132
 
133
133
  export interface ILevelTools {
134
- listLevels(): Promise<any>;
135
- getLevelSummary(levelPath?: string): Promise<any>;
134
+ listLevels(): Promise<StandardActionResponse>;
135
+ getLevelSummary(levelPath?: string): Promise<StandardActionResponse>;
136
136
  registerLight(levelPath: string | undefined, info: { name: string; type: string; details?: Record<string, unknown> }): void;
137
- exportLevel(params: { levelPath?: string; exportPath: string; note?: string; timeoutMs?: number }): Promise<any>;
138
- importLevel(params: { packagePath: string; destinationPath?: string; streaming?: boolean; timeoutMs?: number }): Promise<any>;
139
- saveLevelAs(params: { sourcePath?: string; targetPath: string }): Promise<any>;
140
- deleteLevels(params: { levelPaths: string[] }): Promise<any>;
141
- loadLevel(params: { levelPath: string; streaming?: boolean; position?: [number, number, number] }): Promise<any>;
142
- saveLevel(params: { levelName?: string; savePath?: string }): Promise<any>;
143
- createLevel(params: { levelName: string; template?: 'Empty' | 'Default' | 'VR' | 'TimeOfDay'; savePath?: string }): Promise<any>;
144
- addSubLevel(params: { parentLevel?: string; subLevelPath: string; streamingMethod?: 'Blueprint' | 'AlwaysLoaded' }): Promise<any>;
145
- streamLevel(params: { levelPath?: string; levelName?: string; shouldBeLoaded: boolean; shouldBeVisible?: boolean; position?: [number, number, number] }): Promise<any>;
146
- setupWorldComposition(params: { enableComposition: boolean; tileSize?: number; distanceStreaming?: boolean; streamingDistance?: number }): Promise<any>;
147
- editLevelBlueprint(params: { eventType: 'BeginPlay' | 'EndPlay' | 'Tick' | 'Custom'; customEventName?: string; nodes?: Array<{ nodeType: string; position: [number, number]; connections?: string[] }> }): Promise<any>;
148
- createSubLevel(params: { name: string; type: 'Persistent' | 'Streaming' | 'Lighting' | 'Gameplay'; parent?: string }): Promise<any>;
149
- setWorldSettings(params: { gravity?: number; worldScale?: number; gameMode?: string; defaultPawn?: string; killZ?: number }): Promise<any>;
150
- setLevelBounds(params: { min: [number, number, number]; max: [number, number, number] }): Promise<any>;
151
- buildNavMesh(params: { rebuildAll?: boolean; selectedOnly?: boolean }): Promise<any>;
152
- setLevelVisibility(params: { levelName: string; visible: boolean }): Promise<any>;
153
- setWorldOrigin(params: { location: [number, number, number] }): Promise<any>;
154
- createStreamingVolume(params: { levelName: string; position: [number, number, number]; size: [number, number, number]; streamingDistance?: number }): Promise<any>;
155
- setLevelLOD(params: { levelName: string; lodLevel: number; distance: number }): Promise<any>;
137
+ exportLevel(params: { levelPath?: string; exportPath: string; note?: string; timeoutMs?: number }): Promise<StandardActionResponse>;
138
+ importLevel(params: { packagePath: string; destinationPath?: string; streaming?: boolean; timeoutMs?: number }): Promise<StandardActionResponse>;
139
+ saveLevelAs(params: { sourcePath?: string; targetPath: string }): Promise<StandardActionResponse>;
140
+ deleteLevels(params: { levelPaths: string[] }): Promise<StandardActionResponse>;
141
+ loadLevel(params: { levelPath: string; streaming?: boolean; position?: [number, number, number] }): Promise<StandardActionResponse>;
142
+ saveLevel(params: { levelName?: string; savePath?: string }): Promise<StandardActionResponse>;
143
+ createLevel(params: { levelName: string; template?: 'Empty' | 'Default' | 'VR' | 'TimeOfDay'; savePath?: string }): Promise<StandardActionResponse>;
144
+ addSubLevel(params: { parentLevel?: string; subLevelPath: string; streamingMethod?: 'Blueprint' | 'AlwaysLoaded' }): Promise<StandardActionResponse>;
145
+ streamLevel(params: { levelPath?: string; levelName?: string; shouldBeLoaded: boolean; shouldBeVisible?: boolean; position?: [number, number, number] }): Promise<StandardActionResponse>;
146
+ setupWorldComposition(params: { enableComposition: boolean; tileSize?: number; distanceStreaming?: boolean; streamingDistance?: number }): Promise<StandardActionResponse>;
147
+ editLevelBlueprint(params: { eventType: 'BeginPlay' | 'EndPlay' | 'Tick' | 'Custom'; customEventName?: string; nodes?: Array<{ nodeType: string; position: [number, number]; connections?: string[] }> }): Promise<StandardActionResponse>;
148
+ createSubLevel(params: { name: string; type: 'Persistent' | 'Streaming' | 'Lighting' | 'Gameplay'; parent?: string }): Promise<StandardActionResponse>;
149
+ setWorldSettings(params: { gravity?: number; worldScale?: number; gameMode?: string; defaultPawn?: string; killZ?: number }): Promise<StandardActionResponse>;
150
+ setLevelBounds(params: { min: [number, number, number]; max: [number, number, number] }): Promise<StandardActionResponse>;
151
+ buildNavMesh(params: { rebuildAll?: boolean; selectedOnly?: boolean }): Promise<StandardActionResponse>;
152
+ setLevelVisibility(params: { levelName: string; visible: boolean }): Promise<StandardActionResponse>;
153
+ setWorldOrigin(params: { location: [number, number, number] }): Promise<StandardActionResponse>;
154
+ createStreamingVolume(params: { levelName: string; position: [number, number, number]; size: [number, number, number]; streamingDistance?: number }): Promise<StandardActionResponse>;
155
+ setLevelLOD(params: { levelName: string; lodLevel: number; distance: number }): Promise<StandardActionResponse>;
156
156
  }
157
157
 
158
158
  export interface IEditorTools {
159
159
  isInPIE(): Promise<boolean>;
160
160
  ensureNotInPIE(): Promise<void>;
161
- playInEditor(timeoutMs?: number): Promise<any>;
162
- stopPlayInEditor(): Promise<any>;
163
- pausePlayInEditor(): Promise<any>;
164
- pauseInEditor(): Promise<any>;
165
- buildLighting(): Promise<any>;
166
- setViewportCamera(location?: { x: number; y: number; z: number } | [number, number, number] | null | undefined, rotation?: { pitch: number; yaw: number; roll: number } | [number, number, number] | null | undefined): Promise<any>;
167
- setCameraSpeed(speed: number): Promise<any>;
168
- setFOV(fov: number): Promise<any>;
169
- takeScreenshot(filename?: string, resolution?: string): Promise<any>;
170
- resumePlayInEditor(): Promise<any>;
171
- stepPIEFrame(steps?: number): Promise<any>;
172
- startRecording(options?: { filename?: string; frameRate?: number; durationSeconds?: number; metadata?: Record<string, unknown> }): Promise<any>;
173
- stopRecording(): Promise<any>;
174
- createCameraBookmark(name: string): Promise<any>;
175
- jumpToCameraBookmark(name: string): Promise<any>;
176
- setEditorPreferences(category: string | undefined, preferences: Record<string, unknown>): Promise<any>;
177
- setViewportResolution(width: number, height: number): Promise<any>;
178
- executeConsoleCommand(command: string): Promise<any>;
161
+ playInEditor(timeoutMs?: number): Promise<StandardActionResponse>;
162
+ stopPlayInEditor(): Promise<StandardActionResponse>;
163
+ pausePlayInEditor(): Promise<StandardActionResponse>;
164
+ pauseInEditor(): Promise<StandardActionResponse>;
165
+ buildLighting(): Promise<StandardActionResponse>;
166
+ setViewportCamera(location?: { x: number; y: number; z: number } | [number, number, number] | null | undefined, rotation?: { pitch: number; yaw: number; roll: number } | [number, number, number] | null | undefined): Promise<StandardActionResponse>;
167
+ setCameraSpeed(speed: number): Promise<StandardActionResponse>;
168
+ setFOV(fov: number): Promise<StandardActionResponse>;
169
+ takeScreenshot(filename?: string, resolution?: string): Promise<StandardActionResponse>;
170
+ resumePlayInEditor(): Promise<StandardActionResponse>;
171
+ stepPIEFrame(steps?: number): Promise<StandardActionResponse>;
172
+ startRecording(options?: { filename?: string; frameRate?: number; durationSeconds?: number; metadata?: Record<string, unknown> }): Promise<StandardActionResponse>;
173
+ stopRecording(): Promise<StandardActionResponse>;
174
+ createCameraBookmark(name: string): Promise<StandardActionResponse>;
175
+ jumpToCameraBookmark(name: string): Promise<StandardActionResponse>;
176
+ setEditorPreferences(category: string | undefined, preferences: Record<string, unknown>): Promise<StandardActionResponse>;
177
+ setViewportResolution(width: number, height: number): Promise<StandardActionResponse>;
178
+ executeConsoleCommand(command: string): Promise<StandardActionResponse>;
179
179
  }
180
180
 
181
181
  export interface IEnvironmentTools {
182
- setTimeOfDay(hour: unknown): Promise<any>;
183
- setSunIntensity(intensity: unknown): Promise<any>;
184
- setSkylightIntensity(intensity: unknown): Promise<any>;
185
- exportSnapshot(params: { path?: unknown; filename?: unknown }): Promise<any>;
186
- importSnapshot(params: { path?: unknown; filename?: unknown }): Promise<any>;
187
- cleanup(params?: { names?: unknown }): Promise<any>;
182
+ setTimeOfDay(hour: unknown): Promise<StandardActionResponse>;
183
+ setSunIntensity(intensity: unknown): Promise<StandardActionResponse>;
184
+ setSkylightIntensity(intensity: unknown): Promise<StandardActionResponse>;
185
+ exportSnapshot(params: { path?: unknown; filename?: unknown }): Promise<StandardActionResponse>;
186
+ importSnapshot(params: { path?: unknown; filename?: unknown }): Promise<StandardActionResponse>;
187
+ cleanup(params?: { names?: unknown }): Promise<StandardActionResponse>;
188
188
  }
189
189
 
190
190
  export interface ILandscapeTools {
191
- createLandscape(params: { name: string; location?: [number, number, number]; sizeX?: number; sizeY?: number; quadsPerSection?: number; sectionsPerComponent?: number; componentCount?: number; materialPath?: string; enableWorldPartition?: boolean; runtimeGrid?: string; isSpatiallyLoaded?: boolean; dataLayers?: string[] }): Promise<any>;
192
- sculptLandscape(params: { landscapeName: string; tool: string; brushSize?: number; brushFalloff?: number; strength?: number; location?: [number, number, number]; radius?: number }): Promise<any>;
193
- paintLandscape(params: { landscapeName: string; layerName: string; position: [number, number, number]; brushSize?: number; strength?: number; targetValue?: number; radius?: number; density?: number }): Promise<any>;
194
- createProceduralTerrain(params: { name: string; location?: [number, number, number]; subdivisions?: number; heightFunction?: string; material?: string; settings?: Record<string, unknown> }): Promise<any>;
195
- createLandscapeGrassType(params: { name: string; meshPath: string; density?: number; minScale?: number; maxScale?: number; path?: string; staticMesh?: string }): Promise<any>;
196
- setLandscapeMaterial(params: { landscapeName: string; materialPath: string }): Promise<any>;
197
- modifyHeightmap(params: { landscapeName: string; heightData: number[]; minX: number; minY: number; maxX: number; maxY: number; updateNormals?: boolean }): Promise<any>;
191
+ createLandscape(params: { name: string; location?: [number, number, number]; sizeX?: number; sizeY?: number; quadsPerSection?: number; sectionsPerComponent?: number; componentCount?: number; materialPath?: string; enableWorldPartition?: boolean; runtimeGrid?: string; isSpatiallyLoaded?: boolean; dataLayers?: string[] }): Promise<StandardActionResponse>;
192
+ sculptLandscape(params: { landscapeName: string; tool: string; brushSize?: number; brushFalloff?: number; strength?: number; location?: [number, number, number]; radius?: number }): Promise<StandardActionResponse>;
193
+ paintLandscape(params: { landscapeName: string; layerName: string; position: [number, number, number]; brushSize?: number; strength?: number; targetValue?: number; radius?: number; density?: number }): Promise<StandardActionResponse>;
194
+ createProceduralTerrain(params: { name: string; location?: [number, number, number]; subdivisions?: number; heightFunction?: string; material?: string; settings?: Record<string, unknown> }): Promise<StandardActionResponse>;
195
+ createLandscapeGrassType(params: { name: string; meshPath: string; density?: number; minScale?: number; maxScale?: number; path?: string; staticMesh?: string }): Promise<StandardActionResponse>;
196
+ setLandscapeMaterial(params: { landscapeName: string; materialPath: string }): Promise<StandardActionResponse>;
197
+ modifyHeightmap(params: { landscapeName: string; heightData: number[]; minX: number; minY: number; maxX: number; maxY: number; updateNormals?: boolean }): Promise<StandardActionResponse>;
198
198
  }
199
199
 
200
200
  export interface IFoliageTools {
201
- addFoliageType(params: { name: string; meshPath: string; density?: number; radius?: number; minScale?: number; maxScale?: number; alignToNormal?: boolean; randomYaw?: boolean; groundSlope?: number }): Promise<any>;
202
- addFoliage(params: { foliageType: string; locations: Array<{ x: number; y: number; z: number }> }): Promise<any>;
203
- paintFoliage(params: { foliageType: string; position: [number, number, number]; brushSize?: number; paintDensity?: number; eraseMode?: boolean }): Promise<any>;
204
- createProceduralFoliage(params: { name: string; bounds?: { location: { x: number; y: number; z: number }; size: { x: number; y: number; z: number } }; foliageTypes?: Array<{ meshPath: string; density: number; minScale?: number; maxScale?: number; alignToNormal?: boolean; randomYaw?: boolean }>; volumeName?: string; position?: [number, number, number]; size?: [number, number, number]; seed?: number; tileSize?: number }): Promise<any>;
205
- addFoliageInstances(params: { foliageType: string; transforms: Array<{ location: [number, number, number]; rotation?: [number, number, number]; scale?: [number, number, number] }> }): Promise<any>;
206
- getFoliageInstances(params: { foliageType?: string }): Promise<any>;
207
- removeFoliage(params: { foliageType?: string; removeAll?: boolean }): Promise<any>;
208
- createInstancedMesh(params: { name: string; meshPath: string; instances: Array<{ position: [number, number, number]; rotation?: [number, number, number]; scale?: [number, number, number] }>; enableCulling?: boolean; cullDistance?: number }): Promise<any>;
209
- setFoliageLOD(params: { foliageType: string; lodDistances?: number[]; screenSize?: number[] }): Promise<any>;
210
- setFoliageCollision(params: { foliageType: string; collisionEnabled?: boolean; collisionProfile?: string; generateOverlapEvents?: boolean }): Promise<any>;
211
- createGrassSystem(params: { name: string; grassTypes: Array<{ meshPath: string; density: number; minScale?: number; maxScale?: number }>; windStrength?: number; windSpeed?: number }): Promise<any>;
212
- removeFoliageInstances(params: { foliageType: string; position: [number, number, number]; radius: number }): Promise<any>;
213
- selectFoliageInstances(params: { foliageType: string; position?: [number, number, number]; radius?: number; selectAll?: boolean }): Promise<any>;
214
- updateFoliageInstances(params: { foliageType: string; updateTransforms?: boolean; updateMesh?: boolean; newMeshPath?: string }): Promise<any>;
215
- createFoliageSpawner(params: { name: string; spawnArea: 'Landscape' | 'StaticMesh' | 'BSP' | 'Foliage' | 'All'; excludeAreas?: Array<[number, number, number, number]> }): Promise<any>;
216
- optimizeFoliage(params: { mergeInstances?: boolean; generateClusters?: boolean; clusterSize?: number; reduceDrawCalls?: boolean }): Promise<any>;
201
+ addFoliageType(params: { name: string; meshPath: string; density?: number; radius?: number; minScale?: number; maxScale?: number; alignToNormal?: boolean; randomYaw?: boolean; groundSlope?: number }): Promise<StandardActionResponse>;
202
+ addFoliage(params: { foliageType: string; locations: Array<{ x: number; y: number; z: number }> }): Promise<StandardActionResponse>;
203
+ paintFoliage(params: { foliageType: string; position: [number, number, number]; brushSize?: number; paintDensity?: number; eraseMode?: boolean }): Promise<StandardActionResponse>;
204
+ createProceduralFoliage(params: { name: string; bounds?: { location: { x: number; y: number; z: number }; size: { x: number; y: number; z: number } }; foliageTypes?: Array<{ meshPath: string; density: number; minScale?: number; maxScale?: number; alignToNormal?: boolean; randomYaw?: boolean }>; volumeName?: string; position?: [number, number, number]; size?: [number, number, number]; seed?: number; tileSize?: number }): Promise<StandardActionResponse>;
205
+ addFoliageInstances(params: { foliageType: string; transforms: Array<{ location: [number, number, number]; rotation?: [number, number, number]; scale?: [number, number, number] }> }): Promise<StandardActionResponse>;
206
+ getFoliageInstances(params: { foliageType?: string }): Promise<StandardActionResponse>;
207
+ removeFoliage(params: { foliageType?: string; removeAll?: boolean }): Promise<StandardActionResponse>;
208
+ createInstancedMesh(params: { name: string; meshPath: string; instances: Array<{ position: [number, number, number]; rotation?: [number, number, number]; scale?: [number, number, number] }>; enableCulling?: boolean; cullDistance?: number }): Promise<StandardActionResponse>;
209
+ setFoliageLOD(params: { foliageType: string; lodDistances?: number[]; screenSize?: number[] }): Promise<StandardActionResponse>;
210
+ setFoliageCollision(params: { foliageType: string; collisionEnabled?: boolean; collisionProfile?: string; generateOverlapEvents?: boolean }): Promise<StandardActionResponse>;
211
+ createGrassSystem(params: { name: string; grassTypes: Array<{ meshPath: string; density: number; minScale?: number; maxScale?: number }>; windStrength?: number; windSpeed?: number }): Promise<StandardActionResponse>;
212
+ removeFoliageInstances(params: { foliageType: string; position: [number, number, number]; radius: number }): Promise<StandardActionResponse>;
213
+ selectFoliageInstances(params: { foliageType: string; position?: [number, number, number]; radius?: number; selectAll?: boolean }): Promise<StandardActionResponse>;
214
+ updateFoliageInstances(params: { foliageType: string; updateTransforms?: boolean; updateMesh?: boolean; newMeshPath?: string }): Promise<StandardActionResponse>;
215
+ createFoliageSpawner(params: { name: string; spawnArea: 'Landscape' | 'StaticMesh' | 'BSP' | 'Foliage' | 'All'; excludeAreas?: Array<[number, number, number, number]> }): Promise<StandardActionResponse>;
216
+ optimizeFoliage(params: { mergeInstances?: boolean; generateClusters?: boolean; clusterSize?: number; reduceDrawCalls?: boolean }): Promise<StandardActionResponse>;
217
217
  }
218
218
 
219
219
  export interface ITools {
@@ -5,7 +5,8 @@ export class CommandValidator {
5
5
  'viewmode visualizebuffer worldnormal',
6
6
  'r.gpucrash',
7
7
  'buildpaths', // Can cause access violation if nav system not initialized
8
- 'rebuildnavigation' // Can also crash without proper nav setup
8
+ 'rebuildnavigation', // Can also crash without proper nav setup
9
+ 'obj garbage', 'obj list', 'memreport' // Heavy debug commands that can stall
9
10
  ];
10
11
 
11
12
  private static readonly FORBIDDEN_TOKENS = [
@@ -13,7 +14,7 @@ export class CommandValidator {
13
14
  'rmdir', 'mklink', 'copy ', 'move ', 'start "', 'system(',
14
15
  'import os', 'import subprocess', 'subprocess.', 'os.system',
15
16
  'exec(', 'eval(', '__import__', 'import sys', 'import importlib',
16
- 'with open', 'open('
17
+ 'with open', 'open(', 'write(', 'read('
17
18
  ];
18
19
 
19
20
  private static readonly INVALID_PATTERNS = [
@@ -0,0 +1,162 @@
1
+ /**
2
+ * Unit tests for normalize utility functions
3
+ */
4
+ import { describe, it, expect } from 'vitest';
5
+ import {
6
+ toVec3Object,
7
+ toRotObject,
8
+ toVec3Tuple,
9
+ toRotTuple,
10
+ toFiniteNumber,
11
+ normalizePartialVector,
12
+ normalizeTransformInput
13
+ } from './normalize.js';
14
+
15
+ describe('toVec3Object', () => {
16
+ it('converts object input to Vector3', () => {
17
+ const result = toVec3Object({ x: 1, y: 2, z: 3 });
18
+ expect(result).toEqual({ x: 1, y: 2, z: 3 });
19
+ });
20
+
21
+ it('converts array input to Vector3', () => {
22
+ const result = toVec3Object([1, 2, 3]);
23
+ expect(result).toEqual({ x: 1, y: 2, z: 3 });
24
+ });
25
+
26
+ it('returns null for invalid input', () => {
27
+ expect(toVec3Object('invalid')).toBeNull();
28
+ expect(toVec3Object(null)).toBeNull();
29
+ expect(toVec3Object(undefined)).toBeNull();
30
+ });
31
+
32
+ it('returns null for incomplete object', () => {
33
+ expect(toVec3Object({ x: 1, y: 2 })).toBeNull();
34
+ });
35
+
36
+ it('handles zero values', () => {
37
+ const result = toVec3Object({ x: 0, y: 0, z: 0 });
38
+ expect(result).toEqual({ x: 0, y: 0, z: 0 });
39
+ });
40
+ });
41
+
42
+ describe('toRotObject', () => {
43
+ it('converts object input to Rotator', () => {
44
+ const result = toRotObject({ pitch: 10, yaw: 20, roll: 30 });
45
+ expect(result).toEqual({ pitch: 10, yaw: 20, roll: 30 });
46
+ });
47
+
48
+ it('converts array input to Rotator', () => {
49
+ const result = toRotObject([10, 20, 30]);
50
+ expect(result).toEqual({ pitch: 10, yaw: 20, roll: 30 });
51
+ });
52
+
53
+ it('returns null for invalid input', () => {
54
+ expect(toRotObject('invalid')).toBeNull();
55
+ expect(toRotObject(null)).toBeNull();
56
+ });
57
+ });
58
+
59
+ describe('toVec3Tuple', () => {
60
+ it('converts object to tuple', () => {
61
+ const result = toVec3Tuple({ x: 1, y: 2, z: 3 });
62
+ expect(result).toEqual([1, 2, 3]);
63
+ });
64
+
65
+ it('passes through valid array', () => {
66
+ const result = toVec3Tuple([1, 2, 3]);
67
+ expect(result).toEqual([1, 2, 3]);
68
+ });
69
+
70
+ it('returns null for invalid input', () => {
71
+ expect(toVec3Tuple([1, 2])).toBeNull();
72
+ expect(toVec3Tuple(null)).toBeNull();
73
+ });
74
+ });
75
+
76
+ describe('toRotTuple', () => {
77
+ it('converts object to tuple', () => {
78
+ const result = toRotTuple({ pitch: 10, yaw: 20, roll: 30 });
79
+ expect(result).toEqual([10, 20, 30]);
80
+ });
81
+
82
+ it('passes through valid array', () => {
83
+ const result = toRotTuple([10, 20, 30]);
84
+ expect(result).toEqual([10, 20, 30]);
85
+ });
86
+ });
87
+
88
+ describe('toFiniteNumber', () => {
89
+ it('accepts valid numbers', () => {
90
+ expect(toFiniteNumber(42)).toBe(42);
91
+ expect(toFiniteNumber(0)).toBe(0);
92
+ expect(toFiniteNumber(-5)).toBe(-5);
93
+ expect(toFiniteNumber(3.14)).toBe(3.14);
94
+ });
95
+
96
+ it('parses string numbers', () => {
97
+ expect(toFiniteNumber('42')).toBe(42);
98
+ expect(toFiniteNumber('3.14')).toBe(3.14);
99
+ });
100
+
101
+ it('returns undefined for invalid input', () => {
102
+ expect(toFiniteNumber('not a number')).toBeUndefined();
103
+ expect(toFiniteNumber(NaN)).toBeUndefined();
104
+ expect(toFiniteNumber(Infinity)).toBeUndefined();
105
+ expect(toFiniteNumber(null)).toBeUndefined();
106
+ expect(toFiniteNumber(undefined)).toBeUndefined();
107
+ });
108
+ });
109
+
110
+ describe('normalizePartialVector', () => {
111
+ it('normalizes complete vectors', () => {
112
+ const result = normalizePartialVector({ x: 1, y: 2, z: 3 });
113
+ expect(result).toEqual({ x: 1, y: 2, z: 3 });
114
+ });
115
+
116
+ it('handles partial vectors', () => {
117
+ const result = normalizePartialVector({ x: 1 });
118
+ expect(result).toBeDefined();
119
+ expect(result?.x).toBe(1);
120
+ });
121
+
122
+ it('uses alternate keys for reading input', () => {
123
+ const result = normalizePartialVector(
124
+ { pitch: 10, yaw: 20, roll: 30 },
125
+ ['pitch', 'yaw', 'roll']
126
+ );
127
+ // alternateKeys are used to READ from input, but output is still x/y/z
128
+ expect(result).toEqual({ x: 10, y: 20, z: 30 });
129
+ });
130
+
131
+ it('returns undefined for invalid input', () => {
132
+ expect(normalizePartialVector(null)).toBeUndefined();
133
+ expect(normalizePartialVector('string')).toBeUndefined();
134
+ });
135
+ });
136
+
137
+ describe('normalizeTransformInput', () => {
138
+ it('normalizes complete transform', () => {
139
+ const result = normalizeTransformInput({
140
+ location: { x: 0, y: 0, z: 100 },
141
+ rotation: { pitch: 0, yaw: 90, roll: 0 },
142
+ scale: { x: 1, y: 1, z: 1 }
143
+ });
144
+ expect(result).toBeDefined();
145
+ expect(result?.location).toBeDefined();
146
+ expect(result?.rotation).toBeDefined();
147
+ expect(result?.scale).toBeDefined();
148
+ });
149
+
150
+ it('handles partial transforms', () => {
151
+ const result = normalizeTransformInput({
152
+ location: { x: 100, y: 200, z: 300 }
153
+ });
154
+ expect(result).toBeDefined();
155
+ expect(result?.location).toBeDefined();
156
+ });
157
+
158
+ it('returns undefined for invalid input', () => {
159
+ expect(normalizeTransformInput(null)).toBeUndefined();
160
+ expect(normalizeTransformInput('invalid')).toBeUndefined();
161
+ });
162
+ });
@@ -0,0 +1,43 @@
1
+ export function sanitizePath(path: string, allowedRoots: string[] = ['/Game', '/Engine']): string {
2
+ if (!path || typeof path !== 'string') {
3
+ throw new Error('Invalid path: must be a non-empty string');
4
+ }
5
+
6
+ const trimmed = path.trim();
7
+ if (trimmed.length === 0) {
8
+ throw new Error('Invalid path: cannot be empty');
9
+ }
10
+
11
+ // Normalize separators
12
+ const normalized = trimmed.replace(/\\/g, '/');
13
+
14
+ // Prevent directory traversal
15
+ if (normalized.includes('..')) {
16
+ throw new Error('Invalid path: directory traversal (..) is not allowed');
17
+ }
18
+
19
+ // Ensure path starts with a valid root
20
+ // We check case-insensitive for the root prefix to be user-friendly,
21
+ // but Unreal paths are typically case-insensitive anyway.
22
+ const isAllowed = allowedRoots.some(root =>
23
+ normalized.toLowerCase() === root.toLowerCase() ||
24
+ normalized.toLowerCase().startsWith(`${root.toLowerCase()}/`)
25
+ );
26
+
27
+ if (!isAllowed) {
28
+ throw new Error(`Invalid path: must start with one of [${allowedRoots.join(', ')}]`);
29
+ }
30
+
31
+ // Basic character validation (Unreal strictness)
32
+ // Blocks: < > : " | ? * (Windows reserved) and control characters
33
+ // allowing spaces, dots, underscores, dashes, slashes
34
+ // Note: Unreal allows spaces in some contexts but it's often safer to restrict them if strict mode is desired.
35
+ // For now, we block the definitely invalid ones.
36
+ // eslint-disable-next-line no-control-regex
37
+ const invalidChars = /[<>:"|?*\x00-\x1f]/;
38
+ if (invalidChars.test(normalized)) {
39
+ throw new Error('Invalid path: contains illegal characters');
40
+ }
41
+
42
+ return normalized;
43
+ }