Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions deno.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
"@std/path": "jsr:@std/path@^1.0.8",
"@std/streams": "jsr:@std/streams@^1.0.9",
"@std/testing": "jsr:@std/testing@^1.0.5",
"ueblueprint":"npm:ueblueprint@2.0.0",
"zod": "npm:zod@3.23.8",
"zod-to-json-schema": "npm:zod-to-json-schema@3.23.5",
"ndjson": "https://deno.land/x/ndjson@1.1.0/mod.ts",
Expand Down
76 changes: 75 additions & 1 deletion deno.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion src/commands/pkg.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ export const pkg = new Command<GlobalOptions>()
.option('--profile <profile:string>', 'Build profile', { default: 'client', required: true })
.action(async (options) => {
const { platform, configuration, dryRun, profile, archiveDirectory, zip } = options as PkgOptions
const cfg = await Config.getInstance()
const cfg = Config.getInstance()
const { engine: { path: enginePath }, project: { path: projectPath } } = cfg.mergeConfigCLIConfig({
cliOptions: options,
})
Expand Down
75 changes: 75 additions & 0 deletions src/commands/runpython.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { Command } from '@cliffy/command'
import * as path from '@std/path'
import { findProjectFile } from '../lib/utils.ts'
import { Config } from '../lib/config.ts'
import { logger } from '../lib/logger.ts'
import type { GlobalOptions } from '../lib/types.ts'

import { exec } from '../lib/utils.ts'
import { Engine, getEditorPath } from '../lib/engine.ts'

export type RunPythonOptions = typeof runpython extends
Command<void, void, infer Options, infer Argument, GlobalOptions> ? Options
: never

export const runpython = new Command<GlobalOptions>()
.description('Run Python script in Unreal Engine headless mode')
.option('-s, --script <scriptPath:string>', 'Path to Python script', { required: true })
.option('--stdout', 'Redirect output to stdout', { default: true })
.option('--nosplash', 'Skip splash screen', { default: true })
.option('--nopause', "Don't pause after execution", { default: true })
.option('--nosound', 'Disable sound', { default: true })
.option('--args <args:string>', 'Additional arguments to pass to the script')
.action(async (options) => {
const config = Config.getInstance()
const { engine: { path: enginePath }, project: { path: projectPath } } = config.mergeConfigCLIConfig({
cliOptions: options,
})

const projectFile = await findProjectFile(projectPath).catch(() => {
logger.error(`Could not find project file in ${projectPath}`)
Deno.exit(1)
})

if (!projectFile) {
logger.error(`Could not find project file in ${projectPath}`)
Deno.exit(1)
}

// Resolve absolute path to script
const scriptPath = path.resolve(options.script)

// Get the editor executable path based on platform
const currentPlatform = Engine.getCurrentPlatform()
const editorExePath = getEditorPath(enginePath, currentPlatform)

// Build command arguments
const args = [
projectFile,
'-run=pythonscript',
`-script="${scriptPath}"`,
'-unattended',
]

// Add optional flags
if (options.stdout) args.push('-stdout')
if (options.nosplash) args.push('-nosplash')
if (options.nopause) args.push('-nopause')
if (options.nosound) args.push('-nosound')

// Add any additional arguments
if (options.args) {
args.push(options.args)
}

logger.info(`Running Python script: ${scriptPath}`)
logger.debug(`Full command: ${editorExePath} ${args.join(' ')}`)

try {
const result = await exec(editorExePath, args)
return result
} catch (error: unknown) {
logger.error(`Error running Python script: ${error instanceof Error ? error.message : String(error)}`)
Deno.exit(1)
}
})
4 changes: 2 additions & 2 deletions src/commands/script.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { logger } from '../lib/logger.ts'

import * as esbuild from 'https://deno.land/x/esbuild@v0.24.0/mod.js'

import { denoPlugins } from 'jsr:@luca/esbuild-deno-loader@0.11.0'
import { denoPlugins } from 'jsr:@luca/esbuild-deno-loader@0.11.1'
import { Config } from '../lib/config.ts'

export const script = new Command<GlobalOptions>()
Expand Down Expand Up @@ -44,7 +44,7 @@ export const script = new Command<GlobalOptions>()

const script = (await import(builtOutput)) as Script
await script.main(context)
} catch (e) {
} catch (e: any) {
logger.error(e)
Deno.exit(1)
}
Expand Down
124 changes: 124 additions & 0 deletions src/commands/uasset/extract-eventgraph.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import { Command } from '@cliffy/command'
import type { GlobalOptions } from '../../lib/types.ts'
import { generateBlueprintHtml } from '../../lib/utils.ts'
import { logger } from '../../lib/logger.ts'
import * as path from 'jsr:@std/path'
import * as fs from 'jsr:@std/fs'

export type ExtractEventGraphOptions = typeof extractEventGraph extends
Command<void, void, infer Options extends Record<string, unknown>, [], GlobalOptions> ? Options
: never

/**
* Find the EventGraph section in the exported uasset file as is
* @param fileContent The content of the .copy file as a string
* @returns The EventGraph section as a string, or null if not found
*/
function findEventGraph(fileContent: string): string | null {
// Find the "Begin Object Name="EventGraph"" section
const lines = fileContent.split('\n')
const eventGraphNodes: string[] = []

let insideEventGraph = false
let insideNodeSection = false
let nodeCount = 0
let currentObject = ''
let insideBeginObjectBlock = false

for (let i = 0; i < lines.length; i++) {
const line = lines[i].trim()

if (!line) continue

// Check if we're starting a Begin Object Name="EventGraph" section
if (line.startsWith('Begin Object Name="EventGraph"')) {
insideEventGraph = true
nodeCount = 0
insideNodeSection = false
continue
}

// Skip the end of the EventGraph parent object
if (insideEventGraph && line === 'End Object' && !insideBeginObjectBlock && !insideNodeSection) {
insideEventGraph = false
continue
}

// Check if we're inside an EventGraph and starting a node section with Begin Object Name="..."
if (insideEventGraph && line.startsWith('Begin Object Name="K2Node_')) {
nodeCount++
currentObject = line
eventGraphNodes.push(currentObject)
insideBeginObjectBlock = true
continue
}

// Process EventGraph child blocks that define nodes
if (insideEventGraph && line.startsWith('Begin Object Class="/Script/BlueprintGraph.K2Node_')) {
nodeCount++
currentObject = line
eventGraphNodes.push(currentObject)
insideBeginObjectBlock = true
continue
}

// If we're inside a node definition in the EventGraph, keep collecting its content
if (
insideEventGraph && insideBeginObjectBlock && !line.startsWith('Begin Object') && !line.startsWith('End Object')
) {
eventGraphNodes.push(' ' + line)
continue
}

// Handle the end of a node's Begin/End Object block
if (insideEventGraph && insideBeginObjectBlock && line === 'End Object') {
eventGraphNodes.push(line)
insideBeginObjectBlock = false
currentObject = ''
continue
}
}

// Return the extracted EventGraph nodes, or null if none found
return nodeCount > 0 ? eventGraphNodes.join('\n') : null
}

async function extractEventGraphFromFile(filePath: string, render: boolean) {
const data = await Deno.readTextFile(filePath)
const eventGraph = findEventGraph(data)
if (eventGraph) {
if (render) {
const html = generateBlueprintHtml(eventGraph)
const basename = path.basename(filePath, path.extname(filePath))
const basepath = path.dirname(filePath)
await Deno.writeTextFile(`${basepath}/${basename}.html`, html)
} else {
logger.info(eventGraph)
}
}
}

export const extractEventGraph = new Command<GlobalOptions>()
.description('extract event graph from exported uasset')
.option('-i, --input <file:string>', 'Path to the exported uasset file or directory containing exported uassets', {
required: true,
})
.option('-r, --render', 'Save the output as rendered html', { default: false })
.action(async (options) => {
try {
const isDirectory = await fs.exists(options.input, { isDirectory: true })
if (isDirectory) {
const files = await Deno.readDir(options.input)
for await (const file of files) {
if (file.isFile) {
await extractEventGraphFromFile(path.join(options.input, file.name), options.render)
}
}
} else {
await extractEventGraphFromFile(options.input, options.render)
}
} catch (error: unknown) {
logger.error(`Error parsing blueprint: ${error instanceof Error ? error.message : String(error)}`)
Deno.exit(1)
}
})
15 changes: 15 additions & 0 deletions src/commands/uasset/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { Command } from '@cliffy/command'
import type { GlobalOptions } from '../../lib/types.ts'

import { parse } from './parse.ts'
import { renderBlueprint } from './render-blueprint.ts'
import { extractEventGraph } from './extract-eventgraph.ts'

export const uasset = new Command<GlobalOptions>()
.description('uasset')
.action(function () {
this.showHelp()
})
.command('parse', parse)
.command('render-blueprint', renderBlueprint)
.command('extract-eventgraph', extractEventGraph)
Loading