import { UnrealBridge } from '../unreal-bridge.js';
import { promises as fs } from 'fs';
import path from 'path';
import { bestEffortInterpretedText, coerceNumber, coerceStringArray, interpretStandardResult } from '../utils/result-helpers.js';
export class AssetTools {
constructor(private bridge: UnrealBridge) {}
async importAsset(sourcePath: string, destinationPath: string) {
let createdTestFile = false;
try {
// Sanitize destination path (remove trailing slash) and normalize UE path
let cleanDest = destinationPath.replace(/\/$/, '');
// Map /Content -> /Game for UE asset destinations
if (/^\/?content(\/|$)/i.test(cleanDest)) {
cleanDest = '/Game' + cleanDest.replace(/^\/?content/i, '');
}
// Create test FBX file if it's a test file
if (sourcePath.includes('test_model.fbx')) {
// Create the file outside of Python, before import
try {
await this.createTestFBX(sourcePath);
createdTestFile = true;
} catch (_err) {
// If we can't create the file, we'll handle it in Python
}
}
// Use Python API to import asset with file creation fallback
const pythonCode = `
import unreal
import os
import json
# Create test FBX if needed
source_path = r'${sourcePath}'
if 'test_model.fbx' in source_path and not os.path.exists(source_path):
# Create directory if needed
os.makedirs(os.path.dirname(source_path), exist_ok=True)
# Create a valid FBX ASCII file
fbx_content = """FBXHeaderExtension: {
FBXHeaderVersion: 1003
FBXVersion: 7400
CreationTimeStamp: {
Version: 1000
}
Creator: "MCP FBX Test Generator"
}
GlobalSettings: {
Version: 1000
Properties70: {
P: "UpAxis", "int", "Integer", "",2
P: "UpAxisSign", "int", "Integer", "",1
P: "FrontAxis", "int", "Integer", "",1
P: "FrontAxisSign", "int", "Integer", "",1
P: "CoordAxis", "int", "Integer", "",0
P: "CoordAxisSign", "int", "Integer", "",1
P: "UnitScaleFactor", "double", "Number", "",1
}
}
Definitions: {
Version: 100
Count: 2
ObjectType: "Model" {
Count: 1
}
ObjectType: "Geometry" {
Count: 1
}
}
Objects: {
Geometry: 1234567, "Geometry::Cube", "Mesh" {
Vertices: *24 {
a: -50,-50,50,50,-50,50,50,50,50,-50,50,50,-50,-50,-50,50,-50,-50,50,50,-50,-50,50,-50
}
PolygonVertexIndex: *36 {
a: 0,1,2,-4,4,5,6,-8,0,4,7,-4,1,5,4,-1,2,6,5,-2,3,7,6,-3
}
GeometryVersion: 124
LayerElementNormal: 0 {
Version: 101
Name: ""
MappingInformationType: "ByPolygonVertex"
ReferenceInformationType: "Direct"
Normals: *108 {
a: 0,0,1,0,0,1,0,0,1,0,0,1,0,0,-1,0,0,-1,0,0,-1,0,0,-1,0,-1,0,0,-1,0,0,-1,0,0,-1,0,1,0,0,1,0,0,1,0,0,1,0,0,0,1,0,0,1,0,0,1,0,0,1,0,-1,0,0,-1,0,0,-1,0,0,-1,0,0
}
}
LayerElementUV: 0 {
Version: 101
Name: "UVMap"
MappingInformationType: "ByPolygonVertex"
ReferenceInformationType: "IndexToDirect"
UV: *48 {
a: 0,0,1,0,1,1,0,1,0,0,1,0,1,1,0,1,0,0,1,0,1,1,0,1,0,0,1,0,1,1,0,1,0,0,1,0,1,1,0,1,0,0,1,0,1,1,0,1
}
UVIndex: *36 {
a: 0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35
}
}
Layer: 0 {
Version: 100
LayerElement: {
Type: "LayerElementNormal"
TypedIndex: 0
}
LayerElement: {
Type: "LayerElementUV"
TypedIndex: 0
}
}
}
Model: 2345678, "Model::Cube", "Mesh" {
Version: 232
Properties70: {
P: "InheritType", "enum", "", "",1
P: "ScalingMax", "Vector3D", "Vector", "",0,0,0
P: "DefaultAttributeIndex", "int", "Integer", "",0
}
Shading: Y
Culling: "CullingOff"
}
}
Connections: {
C: "OO",1234567,2345678
}
"""
with open(source_path, 'w') as f:
f.write(fbx_content)
print(f"Created test FBX file: {source_path}")
# Set up the import task
task = unreal.AssetImportTask()
task.filename = r'${sourcePath}'
task.destination_path = '${cleanDest}'
task.automated = True
task.save = True
task.replace_existing = True
# Configure FBX import options
options = unreal.FbxImportUI()
options.import_mesh = True
options.import_as_skeletal = False
options.mesh_type_to_import = unreal.FBXImportType.FBXIT_STATIC_MESH
options.static_mesh_import_data.combine_meshes = True
options.static_mesh_import_data.generate_lightmap_u_vs = False
options.static_mesh_import_data.auto_generate_collision = False
task.options = options
# Use AssetTools to import
asset_tools = unreal.AssetToolsHelpers.get_asset_tools()
result = {'success': False, 'error': 'No assets imported', 'source': task.filename}
try:
asset_tools.import_asset_tasks([task])
if task.imported_object_paths:
result = {
'success': True,
'imported': len(task.imported_object_paths),
'paths': list(task.imported_object_paths)
}
except Exception as e:
result = {'success': False, 'error': str(e), 'source': task.filename}
print('RESULT:' + json.dumps(result))
`.trim();
const pyResp = await this.bridge.executePython(pythonCode);
const interpreted = interpretStandardResult(pyResp, {
successMessage: `Imported assets to ${cleanDest}`,
failureMessage: 'Import failed'
});
if (interpreted.success) {
const count = coerceNumber(interpreted.payload.imported) ?? 0;
const paths = coerceStringArray(interpreted.payload.paths) ?? [];
return {
success: true,
message: `Imported ${count} assets to ${cleanDest}`,
imported: count,
paths
};
}
const errorMessage = `Import failed: ${interpreted.error ?? 'Unknown error'} (source: ${interpreted.payload.source ?? sourcePath})`;
return {
success: false,
message: errorMessage,
error: errorMessage,
details: bestEffortInterpretedText(interpreted)
};
} catch (err) {
return { success: false, error: `Failed to import asset: ${err}` };
} finally {
if (createdTestFile) {
try {
await fs.rm(sourcePath, { force: true });
} catch (cleanupError) {
// Swallow cleanup error but log for debug visibility
console.warn(`Failed to clean up temporary FBX ${sourcePath}:`, cleanupError);
}
}
}
}
async duplicateAsset(sourcePath: string, destinationPath: string) {
try {
const res = await this.bridge.call({
objectPath: '/Script/EditorScriptingUtilities.Default__EditorAssetLibrary',
functionName: 'DuplicateAsset',
parameters: {
SourceAssetPath: sourcePath,
DestinationAssetPath: destinationPath
}
});
return res?.Result ?? res;
} catch (err) {
return { error: `Failed to duplicate asset: ${err}` };
}
}
async deleteAsset(assetPath: string) {
try {
const res = await this.bridge.call({
objectPath: '/Script/EditorScriptingUtilities.Default__EditorAssetLibrary',
functionName: 'DeleteAsset',
parameters: {
AssetPathToDelete: assetPath
}
});
return res?.Result ?? res;
} catch (err) {
return { error: `Failed to delete asset: ${err}` };
}
}
async saveAsset(assetPath: string) {
try {
const res = await this.bridge.call({
objectPath: '/Script/EditorScriptingUtilities.Default__EditorAssetLibrary',
functionName: 'SaveAsset',
parameters: {
AssetToSave: assetPath
}
});
return res?.Result ?? res;
} catch (err) {
return { error: `Failed to save asset: ${err}` };
}
}
private async createTestFBX(filePath: string): Promise<void> {
// Create a minimal valid FBX ASCII file for testing
const fbxContent = `; FBX 7.5.0 project file
FBXHeaderExtension: {
FBXHeaderVersion: 1003
FBXVersion: 7500
CreationTimeStamp: {
Version: 1000
Year: 2024
Month: 1
Day: 1
Hour: 0
Minute: 0
Second: 0
Millisecond: 0
}
Creator: "MCP Test FBX Generator"
}
GlobalSettings: {
Version: 1000
}
Definitions: {
Version: 100
Count: 2
ObjectType: "Model" {
Count: 1
}
ObjectType: "Geometry" {
Count: 1
}
}
Objects: {
Geometry: 1234567, "Geometry::Cube", "Mesh" {
Vertices: *24 {
a: -50,-50,-50,50,-50,-50,50,50,-50,-50,50,-50,-50,-50,50,50,-50,50,50,50,50,-50,50,50
}
PolygonVertexIndex: *36 {
a: 0,1,2,-4,4,7,6,-6,0,4,5,-2,1,5,6,-3,2,6,7,-4,4,0,3,-8
}
GeometryVersion: 124
}
Model: 2345678, "Model::TestCube", "Mesh" {
Version: 232
Properties70: {
P: "ScalingMax", "Vector3D", "Vector", "",0,0,0
P: "DefaultAttributeIndex", "int", "Integer", "",0
}
}
}
Connections: {
C: "OO",1234567,2345678
}
`;
// Ensure directory exists
const dir = path.dirname(filePath);
try {
await fs.mkdir(dir, { recursive: true });
} catch {}
// Write the FBX file
await fs.writeFile(filePath, fbxContent, 'utf8');
}
}