From d39f7eb761a45ef6cbdc48eb3128a2b49b55e5c7 Mon Sep 17 00:00:00 2001 From: charlie-runreal Date: Fri, 23 May 2025 12:24:12 -0500 Subject: [PATCH 1/2] feat: New commands for unreal plugins --- src/cmd.ts | 2 + src/commands/info/index.ts | 4 + src/commands/info/plugin.ts | 28 ++ src/commands/info/project.ts | 23 ++ src/commands/plugin/add.ts | 44 +++ src/commands/plugin/disable.ts | 24 ++ src/commands/plugin/enable.ts | 24 ++ src/commands/plugin/index.ts | 19 ++ src/commands/plugin/info.ts | 10 + src/commands/plugin/list.ts | 148 +++++++++ src/commands/run/client.ts | 2 +- src/commands/run/commandlet.ts | 2 +- src/commands/run/editor.ts | 5 +- src/commands/run/game.ts | 2 +- src/commands/run/python.ts | 2 +- src/commands/run/server.ts | 2 +- src/lib/project-info.ts | 542 +++++++++++++++++++++++++++++++++ src/lib/project.ts | 46 ++- 18 files changed, 921 insertions(+), 8 deletions(-) create mode 100644 src/commands/info/plugin.ts create mode 100644 src/commands/info/project.ts create mode 100644 src/commands/plugin/add.ts create mode 100644 src/commands/plugin/disable.ts create mode 100644 src/commands/plugin/enable.ts create mode 100644 src/commands/plugin/index.ts create mode 100644 src/commands/plugin/info.ts create mode 100644 src/commands/plugin/list.ts create mode 100644 src/lib/project-info.ts diff --git a/src/cmd.ts b/src/cmd.ts index 172fd60..aa6d669 100644 --- a/src/cmd.ts +++ b/src/cmd.ts @@ -6,6 +6,7 @@ import { VERSION } from './version.ts' import { buildgraph } from './commands/buildgraph/index.ts' import { run } from './commands/run/index.ts' +import { plugin } from './commands/plugin/index.ts' import { engine } from './commands/engine/index.ts' import { info } from './commands/info/index.ts' import { build } from './commands/build/index.ts' @@ -57,6 +58,7 @@ export const cli = cmd .command('buildgraph', buildgraph) .command('run', run) .command('engine', engine) + .command('plugin', plugin) .command('build', build) .command('cook', cook) .command('pkg', pkg) diff --git a/src/commands/info/index.ts b/src/commands/info/index.ts index 7adecb5..41fe146 100644 --- a/src/commands/info/index.ts +++ b/src/commands/info/index.ts @@ -4,6 +4,8 @@ import type { GlobalOptions } from '../../lib/types.ts' import { buildId } from './buildId.ts' import { config } from './config.ts' import { listTargets } from './list-targets.ts' +import { project } from './project.ts' +import { plugin } from './plugin.ts' export const info = new Command() .description('info') @@ -13,3 +15,5 @@ export const info = new Command() .command('buildId', buildId) .command('config', config) .command('list-targets', listTargets) + .command('project', project) + .command('plugin', plugin) diff --git a/src/commands/info/plugin.ts b/src/commands/info/plugin.ts new file mode 100644 index 0000000..be1007c --- /dev/null +++ b/src/commands/info/plugin.ts @@ -0,0 +1,28 @@ +import { Command } from '@cliffy/command' +import * as path from '@std/path' + +import type { GlobalOptions } from '../../lib/types.ts' +import { Config } from '../../lib/config.ts' +import { displayUPluginInfo, findPluginFile, readUPluginFile } from '../../lib/project-info.ts' + +export const plugin = new Command() + .description('Displays information about a plugin') + .arguments('') + .action(async (options, pluginName: string) => { + const config = Config.getInstance() + const { engine: { path: enginePath }, project: { path: projectPath } } = config.mergeConfigCLIConfig({ + cliOptions: options, + }) + + const match = await findPluginFile(pluginName, projectPath, enginePath) + if (match) { + const pluginData = await readUPluginFile(match) + if (pluginData) { + displayUPluginInfo(pluginData) + } else { + console.log('plugin could not be loaded') + } + } else { + console.log('could not find plugin') + } + }) diff --git a/src/commands/info/project.ts b/src/commands/info/project.ts new file mode 100644 index 0000000..f395743 --- /dev/null +++ b/src/commands/info/project.ts @@ -0,0 +1,23 @@ +import { Command } from '@cliffy/command' + +import { createProject } from '../../lib/project.ts' +import type { GlobalOptions } from '../../lib/types.ts' +import { Config } from '../../lib/config.ts' +import { displayUProjectInfo, readUProjectFile } from '../../lib/project-info.ts' + +export const project = new Command() + .description('Displays information about the project') + .action(async (options) => { + const config = Config.getInstance() + const { engine: { path: enginePath }, project: { path: projectPath } } = config.mergeConfigCLIConfig({ + cliOptions: options, + }) + const project = await createProject(enginePath, projectPath) + + const projectData = await readUProjectFile(project.projectFileVars.projectFullPath) + if (projectData) { + displayUProjectInfo(projectData) + } else { + console.log('project could not be loaded') + } + }) diff --git a/src/commands/plugin/add.ts b/src/commands/plugin/add.ts new file mode 100644 index 0000000..6e8c235 --- /dev/null +++ b/src/commands/plugin/add.ts @@ -0,0 +1,44 @@ +import { Command } from '@cliffy/command' +import { Config } from '../../lib/config.ts' +import type { GlobalOptions } from '../../lib/types.ts' +import { createProject } from '../../lib/project.ts' +import { readUProjectFile, UnrealEnginePluginReference, UProject, writeUProjectFile } from '../../lib/project-info.ts' +import { exec } from '../../lib/utils.ts' +import * as path from '@std/path' + +export type AddOptions = typeof add extends Command ? Options + : never + +export const add = new Command() + .description('Adds an external plugin to the project') + .arguments(' ') + .option( + '-f, --folder ', + "Plugin subfolder to install to, leaving default will install directly into the project's Plugin folder", + { default: '' }, + ) + .option('-e, --enable', 'Enable this plugin in the project, defaults to true', { default: true }) + .action(async (options, url, pluginName) => { + const { folder, enable } = options as AddOptions + const config = Config.getInstance() + const { engine: { path: enginePath }, project: { path: projectPath } } = config.mergeConfigCLIConfig({ + cliOptions: options, + }) + const project = await createProject(enginePath, projectPath) + + const target_loc = path.relative( + Deno.cwd(), + path.join(project.projectFileVars.projectDir, 'Plugins', folder, pluginName), + ) + + console.log(`installing ${pluginName} to ${target_loc}`) + + await exec('git', ['clone', '--depth', '1', url, target_loc]) + + if (enable) { + project.enablePlugin({ + pluginName: pluginName, + shouldEnable: true, + }) + } + }) diff --git a/src/commands/plugin/disable.ts b/src/commands/plugin/disable.ts new file mode 100644 index 0000000..c4a467a --- /dev/null +++ b/src/commands/plugin/disable.ts @@ -0,0 +1,24 @@ +import { Command } from '@cliffy/command' +import { Config } from '../../lib/config.ts' +import type { GlobalOptions } from '../../lib/types.ts' +import { createProject } from '../../lib/project.ts' +import { readUProjectFile, UnrealEnginePluginReference, UProject, writeUProjectFile } from '../../lib/project-info.ts' + +export type DisableOptions = typeof disable extends Command + ? Options + : never + +export const disable = new Command() + .description('Disables a plugin for the project') + .arguments('') + .action(async (options, target) => { + const config = Config.getInstance() + const { engine: { path: enginePath }, project: { path: projectPath } } = config.mergeConfigCLIConfig({ + cliOptions: options, + }) + const project = await createProject(enginePath, projectPath) + project.enablePlugin({ + pluginName: target, + shouldEnable: false, + }) + }) diff --git a/src/commands/plugin/enable.ts b/src/commands/plugin/enable.ts new file mode 100644 index 0000000..3044ce9 --- /dev/null +++ b/src/commands/plugin/enable.ts @@ -0,0 +1,24 @@ +import { Command } from '@cliffy/command' +import { Config } from '../../lib/config.ts' +import type { GlobalOptions } from '../../lib/types.ts' +import { createProject } from '../../lib/project.ts' +import { readUProjectFile, UnrealEnginePluginReference, UProject, writeUProjectFile } from '../../lib/project-info.ts' + +export type EnableOptions = typeof enable extends Command + ? Options + : never + +export const enable = new Command() + .description('Disables a plugin for the project') + .arguments('') + .action(async (options, target) => { + const config = Config.getInstance() + const { engine: { path: enginePath }, project: { path: projectPath } } = config.mergeConfigCLIConfig({ + cliOptions: options, + }) + const project = await createProject(enginePath, projectPath) + project.enablePlugin({ + pluginName: target, + shouldEnable: true, + }) + }) diff --git a/src/commands/plugin/index.ts b/src/commands/plugin/index.ts new file mode 100644 index 0000000..374b916 --- /dev/null +++ b/src/commands/plugin/index.ts @@ -0,0 +1,19 @@ +import { Command } from '@cliffy/command' +import type { GlobalOptions } from '../../lib/types.ts' + +import { info } from './info.ts' +import { add } from './add.ts' +import { list } from './list.ts' +import { enable } from './enable.ts' +import { disable } from './disable.ts' + +export const plugin = new Command() + .description('Prints information about a plugin') + .action(function () { + this.showHelp() + }) + .command('info', info) + .command('list', list) + .command('enable', enable) + .command('disable', disable) + .command('add', add) diff --git a/src/commands/plugin/info.ts b/src/commands/plugin/info.ts new file mode 100644 index 0000000..44fba2a --- /dev/null +++ b/src/commands/plugin/info.ts @@ -0,0 +1,10 @@ +import { Command } from '@cliffy/command' +import { Config } from '../../lib/config.ts' +import type { GlobalOptions } from '../../lib/types.ts' + +export const info = new Command() + .description('Prints information about a plugin') + .action((options) => { + const config = Config.getInstance() + const cfg = config.mergeConfigCLIConfig({ cliOptions: options }) + }) diff --git a/src/commands/plugin/list.ts b/src/commands/plugin/list.ts new file mode 100644 index 0000000..4fd8c79 --- /dev/null +++ b/src/commands/plugin/list.ts @@ -0,0 +1,148 @@ +import { Command, EnumType } from '@cliffy/command' +import { Config } from '../../lib/config.ts' +import type { GlobalOptions } from '../../lib/types.ts' +import { createProject } from '../../lib/project.ts' +import { findFilesByExtension } from '../../lib/utils.ts' +import * as path from '@std/path' +import { findPluginFile, readUPluginFile, readUProjectFile } from '../../lib/project-info.ts' + +async function getEnabledPlugins(pluginName: string, projectPath: string, enginePath: string, refArray: Array) { + const pluginArray: Array = [] + const match = await findPluginFile(pluginName, projectPath, enginePath) + if (match && match != '') { + const pluginData = await readUPluginFile(match) + + if (pluginData && pluginData.Plugins) { + for (const plugin of pluginData.Plugins) { + if (plugin.Enabled) { + if (![...refArray, ...pluginArray].includes(plugin.Name)) { + pluginArray.push(plugin.Name) + } + } + } + + const subPlugins: Array = [] + for (const pluginName of pluginArray) { + if (![...refArray, ...subPlugins].includes(pluginName)) { + const subPluginArray = await getEnabledPlugins(pluginName, projectPath, enginePath, [ + ...pluginArray, + ...refArray, + ...subPlugins, + ]) + subPlugins.push(...subPluginArray) + } + } + pluginArray.push(...subPlugins) + } + } else { + console.log('could not find plugin') + } + return pluginArray +} + +enum ListTarget { + All = 'all', + Referenced = 'referenced', + Project = 'project', + Engine = 'engine', + Default = 'default', +} + +export type ListOptions = typeof list extends Command + ? Options + : never + +export const list = new Command() + .description(` + Lists plugins + target - defaults to referenced: + * all - Lists all plugins from the engine and project + * referenced - Lists all plugins referenced by the project + * engine - Lists all engine plugins + * project - Lists only plugins in the project plugins + * default - Lists all plugins that are enabled by default + `) + .type('ListTarget', new EnumType(ListTarget)) + .arguments('') + .option('-r, --recursive', 'List all nested plugins that are enabled when used with "referenced" or "default"', { + default: false, + }) + .action(async (options, target = ListTarget.Project) => { + const { recursive } = options as ListOptions + const config = Config.getInstance() + const { engine: { path: enginePath }, project: { path: projectPath } } = config.mergeConfigCLIConfig({ + cliOptions: options, + }) + const project = await createProject(enginePath, projectPath) + const projectData = await readUProjectFile(project.projectFileVars.projectFullPath) + + switch (target) { + case ListTarget.All: { + const projectPlugins = await findFilesByExtension(path.join(projectPath, 'Plugins'), 'uplugin', true) + const enginePlugins = await findFilesByExtension(path.join(enginePath, 'Engine', 'Plugins'), 'uplugin', true) + console.log('Project Plugins:\n') + projectPlugins.forEach((plugin) => { + console.log(path.basename(plugin, '.uplugin')) + }) + console.log('Engine Plugins:\n') + enginePlugins.forEach((plugin) => { + console.log(path.basename(plugin, '.uplugin')) + }) + break + } + case ListTarget.Referenced: { + const allEnabledPlugins: Array = [] + + if (projectData && projectData.Plugins) { + for (const plugin of projectData.Plugins) { + if (plugin.Enabled) { + allEnabledPlugins.push(plugin.Name) + } + if (recursive) { + const enabledPlugins = await getEnabledPlugins(plugin.Name, projectPath, enginePath, allEnabledPlugins) + allEnabledPlugins.push(...enabledPlugins) + } + } + } + const uniquePlugins = [...new Set(allEnabledPlugins)] + console.log(uniquePlugins) + break + } + case ListTarget.Project: { + const projectPlugins = await findFilesByExtension(path.join(projectPath, 'Plugins'), 'uplugin', true) + console.log('Project Plugins:\n') + projectPlugins.forEach((plugin) => { + console.log(path.basename(plugin, '.uplugin')) + }) + break + } + case ListTarget.Engine: { + const enginePlugins = await findFilesByExtension(path.join(enginePath, 'Engine', 'Plugins'), 'uplugin', true) + console.log('Engine Plugins:\n') + enginePlugins.forEach((plugin) => { + console.log(path.basename(plugin, '.uplugin')) + }) + break + } + case ListTarget.Default: { + const projectPlugins = await findFilesByExtension(path.join(projectPath, 'Plugins'), 'uplugin', true) + console.log('Project Plugins enabled by default:\n') + for (const plugin of projectPlugins) { + const uplugin = await readUPluginFile(plugin) + if (uplugin && uplugin.EnabledByDefault) { + console.log(path.basename(plugin, '.uplugin')) + } + } + console.log('Engine Plugins:\n') + const enginePlugins = await findFilesByExtension(path.join(enginePath, 'Engine', 'Plugins'), 'uplugin', true) + for (const plugin of enginePlugins) { + const uplugin = await readUPluginFile(plugin) + if (uplugin && uplugin.EnabledByDefault) { + console.log(path.basename(plugin, '.uplugin')) + console.log(uplugin.EnabledByDefault) + } + } + break + } + } + }) diff --git a/src/commands/run/client.ts b/src/commands/run/client.ts index 2b8b6b4..27bbac6 100644 --- a/src/commands/run/client.ts +++ b/src/commands/run/client.ts @@ -28,7 +28,7 @@ export const client = new Command() await project.compile({ target: EngineTarget.Client, configuration: configuration as EngineConfiguration, - dryRun: options.dryRun, + dryRun: dryRun, }) } diff --git a/src/commands/run/commandlet.ts b/src/commands/run/commandlet.ts index 8e33f8b..a5002ed 100644 --- a/src/commands/run/commandlet.ts +++ b/src/commands/run/commandlet.ts @@ -34,7 +34,7 @@ export const commandlet = new Command() await project.compile({ target: EngineTarget.Editor, configuration: configuration as EngineConfiguration, - dryRun: options.dryRun, + dryRun: dryRun, }) } diff --git a/src/commands/run/editor.ts b/src/commands/run/editor.ts index 6cd3c39..7b5178e 100644 --- a/src/commands/run/editor.ts +++ b/src/commands/run/editor.ts @@ -24,13 +24,16 @@ export const editor = new Command() const project = await createProject(enginePath, projectPath) + console.log(compile) if (compile) { + console.log('compiling') await project.compile({ target: EngineTarget.Editor, configuration: configuration as EngineConfiguration, - dryRun: options.dryRun, + dryRun: dryRun, }) } + console.log('running') await project.runEditor({ extraArgs: [...runArguments] }) }) diff --git a/src/commands/run/game.ts b/src/commands/run/game.ts index 7fdfe71..ec5b2d1 100644 --- a/src/commands/run/game.ts +++ b/src/commands/run/game.ts @@ -28,7 +28,7 @@ export const game = new Command() await project.compile({ target: EngineTarget.Game, configuration: configuration as EngineConfiguration, - dryRun: options.dryRun, + dryRun: dryRun, }) } diff --git a/src/commands/run/python.ts b/src/commands/run/python.ts index 3642964..c031b04 100644 --- a/src/commands/run/python.ts +++ b/src/commands/run/python.ts @@ -29,7 +29,7 @@ export const python = new Command() await project.compile({ target: EngineTarget.Editor, configuration: configuration as EngineConfiguration, - dryRun: options.dryRun, + dryRun: dryRun, }) } diff --git a/src/commands/run/server.ts b/src/commands/run/server.ts index 425b711..5ae5243 100644 --- a/src/commands/run/server.ts +++ b/src/commands/run/server.ts @@ -28,7 +28,7 @@ export const server = new Command() await project.compile({ target: EngineTarget.Server, configuration: configuration as EngineConfiguration, - dryRun: options.dryRun, + dryRun: dryRun, }) } diff --git a/src/lib/project-info.ts b/src/lib/project-info.ts new file mode 100644 index 0000000..73f0dee --- /dev/null +++ b/src/lib/project-info.ts @@ -0,0 +1,542 @@ +import * as path from '@std/path' + +import { findFilesByExtension } from './utils.ts' + +/** + * Enum for target types + */ +type TargetType = 'Unknown' | 'Game' | 'Server' | 'Client' | 'Editor' | 'Program' + +/** + * Enum for target configurations + */ +type TargetConfiguration = 'Unknown' | 'Debug' | 'DebugGame' | 'Development' | 'Shipping' | 'Test' + +/** + * Enum for module types + */ +type ModuleType = + | 'Runtime' + | 'RuntimeNoCommandlet' + | 'RuntimeAndProgram' + | 'CookedOnly' + | 'UncookedOnly' + | 'Developer' + | 'DeveloperTool' + | 'Editor' + | 'EditorNoCommandlet' + | 'EditorAndProgram' + | 'Program' + | 'ServerOnly' + | 'ClientOnly' + | 'ClientOnlyNoCommandlet' + +/** + * Enum for loading phases + */ +type LoadingPhase = + | 'EarliestPossible' + | 'PostConfigInit' + | 'PostSplashScreen' + | 'PreEarlyLoadingScreen' + | 'PreLoadingScreen' + | 'PreDefault' + | 'Default' + | 'PostDefault' + | 'PostEngineInit' + | 'None' + +/** + * Enum for localization target loading policy + */ +type LoadingPolicy = + | 'Never' + | 'Always' + | 'Editor' + | 'Game' + | 'PropertyNames' + | 'ToolTips' + +/** + * Description of a loadable Unreal Engine module + */ +interface UnrealEngineModule { + /** Name of the module */ + Name: string + + /** Usage type of module */ + Type: ModuleType + + /** When should the module be loaded during the startup sequence? This is sort of an advanced setting. */ + LoadingPhase?: LoadingPhase + + /** List of allowed platforms */ + PlatformAllowList?: string[] + + /** @deprecated Use "PlatformAllowList" instead. List of allowed platforms */ + WhitelistPlatforms?: string[] + + /** List of disallowed platforms */ + PlatformDenyList?: string[] + + /** @deprecated Use "PlatformDenyList" instead. List of disallowed platforms */ + BlacklistPlatforms?: string[] + + /** List of allowed targets */ + TargetAllowList?: TargetType[] + + /** @deprecated Use "TargetAllowList" instead. List of allowed targets */ + WhitelistTargets?: TargetType[] + + /** List of disallowed targets */ + TargetDenyList?: TargetType[] + + /** @deprecated Use "TargetDenyList" instead. List of disallowed targets */ + BlacklistTargets?: TargetType[] + + /** List of allowed target configurations */ + TargetConfigurationAllowList?: TargetConfiguration[] + + /** @deprecated Use "TargetConfigurationAllowList" instead. List of allowed target configurations */ + WhitelistTargetConfigurations?: TargetConfiguration[] + + /** List of disallowed target configurations */ + TargetConfigurationDenyList?: TargetConfiguration[] + + /** @deprecated Use "TargetConfigurationDenyList" instead. List of disallowed target configurations */ + BlacklistTargetConfigurations?: TargetConfiguration[] + + /** List of allowed programs */ + ProgramAllowList?: string[] + + /** @deprecated Use "ProgramAllowList" instead. List of allowed programs */ + WhitelistPrograms?: string[] + + /** List of allowed programs */ + ProgramDenyList?: string[] + + /** @deprecated Use "ProgramDenyList" instead. List of allowed programs */ + BlacklistPrograms?: string[] + + /** List of additional dependencies for building this module */ + AdditionalDependencies?: string[] + + /** When true, an empty PlatformAllowList is interpreted as 'no platforms' with the expectation that explicit platforms will be added in plugin extensions */ + HasExplicitPlatforms?: boolean +} + +/** + * Description of a localization target + */ +interface LocalizationTarget { + /** Name of this target */ + Name: string + + /** Policy by which the localization data associated with a target should be loaded */ + LoadingPolicy?: LoadingPolicy +} + +/** + * Description for a Unreal Engine plugin reference + * Contains the information required to enable or disable a plugin for a given platform + */ +export interface UnrealEnginePluginReference { + /** Name of the plugin */ + Name: string + + /** Whether plugin should be enabled by default */ + Enabled: boolean + + /** Whether this plugin is optional, and the game should silently ignore it not being present */ + Optional?: boolean + + /** Description of the plugin for users that do not have it installed */ + Description?: string + + /** URL for this plugin on the marketplace, if the user doesn't have it installed */ + MarketplaceURL?: string + + /** List of platforms for which the plugin should be enabled (or all platforms if blank) */ + PlatformAllowList?: string[] + + /** @deprecated Use "PlatformAllowList" instead. List of platforms for which the plugin should be enabled (or all platforms if blank) */ + WhitelistPlatforms?: string[] + + /** List of target configurations for which the plugin should be disabled */ + PlatformDenyList?: string[] + + /** @deprecated Use "PlatformDenyList" instead. List of target configurations for which the plugin should be disabled */ + BlacklistPlatforms?: string[] + + /** List of target configurations for which the plugin should be enabled (or all target configurations if blank) */ + TargetConfigurationAllowList?: TargetConfiguration[] + + /** @deprecated Use "TargetConfigurationAllowList" instead. List of target configurations for which the plugin should be enabled (or all target configurations if blank) */ + WhitelistTargetConfigurations?: TargetConfiguration[] + + /** List of target configurations for which the plugin should be disabled */ + TargetConfigurationDenyList?: TargetConfiguration[] + + /** @deprecated Use "TargetConfigurationDenyList" instead. List of target configurations for which the plugin should be disabled */ + BlacklistTargetConfigurations?: TargetConfiguration[] + + /** List of targets for which the plugin should be enabled (or all targets if blank) */ + TargetAllowList?: TargetType[] + + /** @deprecated Use "TargetAllowList" instead. List of targets for which the plugin should be enabled (or all targets if blank) */ + WhitelistTargets?: TargetType[] + + /** List of targets for which the plugin should be disabled */ + TargetDenyList?: TargetType[] + + /** @deprecated Use "TargetDenyList" instead. List of targets for which the plugin should be disabled */ + BlacklistTargets?: TargetType[] + + /** The list of supported target platforms for this plugin. This field is copied from the plugin descriptor, and supplements the user's whitelisted and blacklisted platforms */ + SupportedTargetPlatforms?: string[] + + /** When true, empty SupportedTargetPlatforms and PlatformAllowList are interpreted as 'no platforms' with the expectation that explicit platforms will be added in plugin platform extensions */ + HasExplicitPlatforms?: boolean +} + +/** + * Type for build steps mapping + */ +type BuildSteps = { + [hostPlatform: string]: string[] +} + +/** + * Unreal Engine Project Description File + */ +export interface UPlugin { + /** Descriptor version number */ + FileVersion: number + + /** Version number for the plugin. The version number must increase with every version of the plugin, so that the system can determine whether one version of a plugin is newer than another, or to enforce other requirements. This version number is not displayed in front-facing UI. Use the VersionName for that. */ + Version?: number + + /** Name of the version for this plugin. This is the front-facing part of the version number. It doesn't need to match the version number numerically, but should be updated when the version number is increased accordingly. */ + VersionName?: string + + /** Friendly name of the plugin */ + FriendlyName?: string + + /** Description of the plugin */ + Description?: string + + /** The name of the category this plugin */ + Category?: string + + /** @deprecated Use "Category" instead. The name of the category this plugin */ + CategoryPath?: string + + /** The company or individual who created this plugin. This is an optional field that may be displayed in the user interface. */ + CreatedBy?: string + + /** Hyperlink URL string for the company or individual who created this plugin. This is optional. */ + CreatedByURL?: string + + /** Hyperlink URL string for documentation about this plugin */ + DocsURL?: string + + /** Marketplace URL for this plugin. This URL will be embedded into projects that enable this plugin, so we can redirect to the marketplace if a user doesn't have it installed. */ + MarketplaceURL?: string + + /** Support URL/email for this plugin */ + SupportURL?: string + + /** Version of the engine that this plugin is compatible with */ + EngineVersion?: string + + /** Optional custom virtual path to display in editor to better organize. Inserted just before this plugin's directory in the path: /All/Plugins/EditorCustomVirtualPath/PluginName */ + EditorCustomVirtualPath?: string + + /** List of target platforms supported by this plugin. This list will be copied to any plugin reference from a project file, to allow filtering entire plugins from staged builds. */ + SupportedTargetPlatforms?: string[] + + /** List of programs that are supported by this plugin */ + SupportedPrograms?: string[] + + /** The real plugin that this one is just extending */ + ParentPluginName?: string + + /** If true, this plugin is from a platform extension extending another plugin */ + bIsPluginExtension?: boolean + + /** List of all modules associated with this plugin */ + Modules?: UnrealEngineModule[] + + /** List of all localization targets associated with this plugin */ + LocalizationTargets?: LocalizationTarget[] + + /** Whether this plugin should be enabled by default for all projects */ + EnabledByDefault?: boolean + + /** Can this plugin contain content? */ + CanContainContent?: boolean + + /** Can this plugin contain Verse code? */ + CanContainVerse?: boolean + + /** Marks the plugin as beta in the UI */ + IsBetaVersion?: boolean + + /** Marks the plugin as experimental in the UI */ + IsExperimentalVersion?: boolean + + /** Signifies that the plugin was installed on top of the engine */ + Installed?: boolean + + /** For plugins that are under a platform folder (eg. /PS4/), determines whether compiling the plugin requires the build platform and/or SDK to be available */ + RequiresBuildPlatform?: boolean + + /** For auto-generated plugins that should not be listed in the plugin browser for users to disable freely */ + Hidden?: boolean + + /** When true, this plugin's modules will not be loaded automatically nor will it's content be mounted automatically. It will load/mount when explicitly requested and LoadingPhases will be ignored */ + ExplicitlyLoaded?: boolean + + /** When true, an empty SupportedTargetPlatforms is interpreted as 'no platforms' with the expectation that explicit platforms will be added in plugin platform extensions */ + HasExplicitPlatforms?: boolean + + /** @deprecated Add "UnrealHeaderTool" to "SupportedPrograms" instead. Marks this plugin as supporting the UnrealHeaderTool */ + CanBeUsedWithUnrealHeaderTool?: boolean + + /** Custom steps to execute before building targets in this plugin. A mapping from host platform to a list of commands */ + PreBuildSteps?: BuildSteps + + /** Custom steps to execute after building targets in this plugin. A mapping from host platform to a list of commands */ + PostBuildSteps?: BuildSteps + + /** List of dependent plugins */ + Plugins?: UnrealEnginePluginReference[] + + /** Additional properties are allowed */ + [key: string]: any +} + +/** + * Unreal Engine Project Description File (.uproject) + */ +export interface UProject { + /** Descriptor version number */ + FileVersion: number + + /** The engine to open the project with */ + EngineAssociation?: string + + /** Category to show under the project browser */ + Category?: string + + /** Description to show in the project browser */ + Description?: string + + /** Indicates if this project is an Enterprise project */ + Enterprise?: boolean + + /** Indicates that enabled by default engine plugins should not be enabled unless explicitly enabled by the project or target files */ + DisableEnginePluginsByDefault?: boolean + + /** List of all modules associated with this project */ + Modules?: UnrealEngineModule[] + + /** List of plugins for this project (may be enabled/disabled) */ + Plugins?: UnrealEnginePluginReference[] + + /** List of additional directories to scan for plugins */ + AdditionalPluginDirectories?: string[] + + /** List of additional root directories to scan for modules */ + AdditionalRootDirectories?: string[] + + /** Array of platforms that this project is targeting */ + TargetPlatforms?: string[] + + /** A hash that is used to determine if the project was forked from a sample */ + EpicSampleNameHash?: number + + /** Custom steps to execute before building targets in this project. A mapping from host platform to a list of commands. */ + PreBuildSteps?: BuildSteps + + /** Custom steps to execute after building targets in this project. A mapping from host platform to a list of commands. */ + PostBuildSteps?: BuildSteps +} + +export async function readUPluginFile(filePath: string): Promise { + try { + // Read the file + const text = await Deno.readTextFile(filePath) + + // Parse the JSON content + const upluginData: UPlugin = await JSON.parse(text) + + return upluginData + } catch (error) { + if (error instanceof Deno.errors.NotFound) { + //console.warn(`File not found: ${filePath}`) + return null + } else if (error instanceof SyntaxError) { + //console.warn(`${filePath} Invalid .uplugin file format: ${error.message}`) + return null + } else { + //console.warn(`${filePath} Error reading .uplugin file`) + return null + } + } +} + +export function displayUPluginInfo(uplugin: UPlugin): void { + console.log('\n=== UPlugin Information ===') + console.log(`File Version: ${uplugin.FileVersion}`) + console.log(`Version: ${uplugin.Version}`) + console.log(`Version Name: ${uplugin.VersionName}`) + console.log(`Friendly Name: ${uplugin.FriendlyName}`) + console.log(`Description: ${uplugin.Description}`) + console.log(`Category: ${uplugin.Category}`) + console.log(`Created By: ${uplugin.CreatedBy}`) + console.log(`Created By URL: ${uplugin.CreatedByURL}`) + + if (uplugin.DocsURL) console.log(`Docs URL: ${uplugin.DocsURL}`) + if (uplugin.MarketplaceURL) console.log(`Marketplace URL: ${uplugin.MarketplaceURL}`) + if (uplugin.SupportURL) console.log(`Support URL: ${uplugin.SupportURL}`) + + console.log(`Can Contain Content: ${uplugin.CanContainContent}`) + + if (uplugin.IsBetaVersion !== undefined) console.log(`Is Beta Version: ${uplugin.IsBetaVersion}`) + if (uplugin.IsExperimentalVersion !== undefined) { + console.log(`Is Experimental Version: ${uplugin.IsExperimentalVersion}`) + } + if (uplugin.Installed !== undefined) console.log(`Installed: ${uplugin.Installed}`) + if (uplugin.EnabledByDefault !== undefined) console.log(`Enabled By Default: ${uplugin.EnabledByDefault}`) + + if (uplugin.Modules && uplugin.Modules.length > 0) { + console.log('\n--- Modules ---') + uplugin.Modules.forEach((module, index) => { + console.log(`\nModule ${index + 1}:`) + console.log(` Name: ${module.Name}`) + console.log(` Type: ${module.Type}`) + + if (module.LoadingPhase) { + console.log(` Loading Phase: ${module.LoadingPhase}`) + } + + if (module.PlatformAllowList && module.PlatformAllowList.length > 0) { + console.log(` Platform Allow List: ${module.PlatformAllowList.join(', ')}`) + } + + if (module.PlatformDenyList && module.PlatformDenyList.length > 0) { + console.log(` Platform Deny List: ${module.PlatformDenyList.join(', ')}`) + } + + // Handle legacy platform lists + if (module.WhitelistPlatforms && module.WhitelistPlatforms.length > 0) { + console.log(` Whitelist Platforms (Legacy): ${module.WhitelistPlatforms.join(', ')}`) + } + + if (module.BlacklistPlatforms && module.BlacklistPlatforms.length > 0) { + console.log(` Blacklist Platforms (Legacy): ${module.BlacklistPlatforms.join(', ')}`) + } + }) + } +} + +export async function readUProjectFile(filePath: string): Promise { + try { + // Read the file + const text = await Deno.readTextFile(filePath) + + // Parse the JSON content + const uprojectData: UProject = JSON.parse(text) + + return uprojectData + } catch (error) { + if (error instanceof Deno.errors.NotFound) { + //console.warn(`File not found: ${filePath}`) + return null + } else if (error instanceof SyntaxError) { + //console.warn(`Invalid .uproject file format: ${error.message}`) + return null + } else { + //console.warn(`Error reading .uproject file`) + return null + } + } +} + +export async function writeUProjectFile(filePath: string, projectData: UProject) { + await Deno.writeTextFile(filePath, JSON.stringify(projectData)) +} + +export function displayUProjectInfo(uproject: UProject): void { + console.log('\n=== UProject Information ===') + console.log(`File Version: ${uproject.FileVersion}`) + console.log(`Engine Association: ${uproject.EngineAssociation}`) + console.log(`Category: ${uproject.Category}`) + console.log(`Description: ${uproject.Description}`) + + if (uproject.Modules && uproject.Modules.length > 0) { + console.log('\n--- Modules ---') + uproject.Modules.forEach((module, index) => { + console.log(`\nModule ${index + 1}:`) + console.log(` Name: ${module.Name}`) + console.log(` Type: ${module.Type}`) + if (module.LoadingPhase) { + console.log(` Loading Phase: ${module.LoadingPhase}`) + } + }) + } + + if (uproject.Plugins && uproject.Plugins.length > 0) { + console.log('\n--- Plugins ---') + uproject.Plugins.forEach((plugin, index) => { + console.log(`\nPlugin ${index + 1}:`) + console.log(` Name: ${plugin.Name}`) + console.log(` Enabled: ${plugin.Enabled}`) + }) + } + + if (uproject.TargetPlatforms && uproject.TargetPlatforms.length > 0) { + console.log('\n--- Target Platforms ---') + uproject.TargetPlatforms.forEach((platform, index) => { + console.log(` ${index + 1}. ${platform}`) + }) + } +} + +export async function findPluginFile( + pluginName: string, + projectPath: string, + enginePath?: string, +): Promise { + const pluginFiles = await findFilesByExtension(path.join(projectPath, 'Plugins'), 'uplugin', true) + const regex = new RegExp(`${pluginName}\.uplugin`) + const matches = pluginFiles.filter((element) => regex.test(element)) + + if (matches.length <= 0 && enginePath) { + const enginePlugins = await findFilesByExtension(path.join(enginePath, 'Engine', 'Plugins'), 'uplugin', true) + const engineMatches = enginePlugins.filter((element) => regex.test(element)) + matches.push(...engineMatches) + } + + if (matches.length == 1) { + return matches[0] + } else if (matches.length > 1) { + console.log(`found more than one plugin with name ${pluginName}`) + console.log(matches) + return null + } else { + console.log(`could not find ${pluginName}`) + return null + } +} + +// export async function displayInfo(filePath: string, jsonOutput: boolean = false) { +// if (!filePath.endsWith('.uproject')) { +// console.warn("Warning: The provided file doesn't have a .uproject extension") +// } + +// const uprojectData = await readUProjectFile(filePath) +// displayUProjectInfo(uprojectData) +// console.log(JSON.stringify(uprojectData, null, 2)) +// } diff --git a/src/lib/project.ts b/src/lib/project.ts index 5647d5d..00bf2b8 100644 --- a/src/lib/project.ts +++ b/src/lib/project.ts @@ -14,6 +14,7 @@ import { TargetInfo, } from '../lib/engine.ts' import { copyBuildGraphScripts, exec, findProjectFile, parseCSForTargetType } from '../lib/utils.ts' +import { readUProjectFile, UnrealEnginePluginReference, UProject, writeUProjectFile } from './project-info.ts' const TargetError = (target: string, targets: string[]) => { return new ValidationError(`Invalid Target: ${target} @@ -83,6 +84,44 @@ export class Project { this.projectFileVars = projectFileVars } + async enablePlugin({ + pluginName, + shouldEnable = true, + }: { + pluginName: string + shouldEnable?: boolean + }) { + const projectData = await readUProjectFile(this.projectFileVars.projectFullPath) + + let foundPlugin = false + + if (projectData && projectData.Plugins) { + for (const plugin of projectData.Plugins) { + if (plugin.Name === pluginName) { + console.log('found plugin') + foundPlugin = true + if (plugin.Enabled == shouldEnable) { + console.log(`plugin was already ${shouldEnable}d. Exiting.`) + Deno.exit() + } + plugin.Enabled = shouldEnable + break + } + } + if (!foundPlugin) { + console.log(`could not find ${pluginName} in project plugin list, adding new entry`) + const newPlugin: UnrealEnginePluginReference = { + Name: pluginName, + Enabled: shouldEnable, + } + projectData.Plugins.push(newPlugin) + } + writeUProjectFile(this.projectFileVars.projectFullPath, projectData as UProject) + } else { + console.log('failed to parse project file') + } + } + async compile({ target = EngineTarget.Editor, configuration = EngineConfiguration.Development, @@ -106,11 +145,14 @@ export class Project { }) { const projectTarget = await this.getProjectTarget(target) - this.compileTarget({ + const targetFullString = + `\-Target="${projectTarget} ${platform} ${configuration} ${this.projectFileVars.projectArgument}\"` + + await this.compileTarget({ target: projectTarget, configuration: configuration, platform: platform, - extraArgs: extraArgs, + extraArgs: [targetFullString, ...extraArgs], dryRun: dryRun, clean: clean, nouht: nouht, From ac96990da518de13cebcceb0a75c4985729d8af5 Mon Sep 17 00:00:00 2001 From: charlie-runreal Date: Fri, 23 May 2025 15:51:37 -0500 Subject: [PATCH 2/2] ci: Cleaned up some extra logging --- src/commands/info/plugin.ts | 4 ++-- src/commands/info/project.ts | 2 +- src/commands/plugin/list.ts | 6 +++--- src/commands/run/editor.ts | 3 --- src/lib/project.ts | 6 ++---- 5 files changed, 8 insertions(+), 13 deletions(-) diff --git a/src/commands/info/plugin.ts b/src/commands/info/plugin.ts index be1007c..202f8e5 100644 --- a/src/commands/info/plugin.ts +++ b/src/commands/info/plugin.ts @@ -20,9 +20,9 @@ export const plugin = new Command() if (pluginData) { displayUPluginInfo(pluginData) } else { - console.log('plugin could not be loaded') + console.log(`The plugin ${pluginName} could not be loaded`) } } else { - console.log('could not find plugin') + console.log(`Unable to find the plugin ${pluginName}`) } }) diff --git a/src/commands/info/project.ts b/src/commands/info/project.ts index f395743..7a6420c 100644 --- a/src/commands/info/project.ts +++ b/src/commands/info/project.ts @@ -18,6 +18,6 @@ export const project = new Command() if (projectData) { displayUProjectInfo(projectData) } else { - console.log('project could not be loaded') + console.log('The project file could not be loaded') } }) diff --git a/src/commands/plugin/list.ts b/src/commands/plugin/list.ts index 4fd8c79..08126e3 100644 --- a/src/commands/plugin/list.ts +++ b/src/commands/plugin/list.ts @@ -35,7 +35,7 @@ async function getEnabledPlugins(pluginName: string, projectPath: string, engine pluginArray.push(...subPlugins) } } else { - console.log('could not find plugin') + console.log(`Unable to find the plugin ${pluginName}`) } return pluginArray } @@ -105,6 +105,7 @@ export const list = new Command() } } const uniquePlugins = [...new Set(allEnabledPlugins)] + console.log('All Referenced Plugins:') console.log(uniquePlugins) break } @@ -133,13 +134,12 @@ export const list = new Command() console.log(path.basename(plugin, '.uplugin')) } } - console.log('Engine Plugins:\n') + console.log('Engine Plugins enabled by default:\n') const enginePlugins = await findFilesByExtension(path.join(enginePath, 'Engine', 'Plugins'), 'uplugin', true) for (const plugin of enginePlugins) { const uplugin = await readUPluginFile(plugin) if (uplugin && uplugin.EnabledByDefault) { console.log(path.basename(plugin, '.uplugin')) - console.log(uplugin.EnabledByDefault) } } break diff --git a/src/commands/run/editor.ts b/src/commands/run/editor.ts index 7b5178e..be7734b 100644 --- a/src/commands/run/editor.ts +++ b/src/commands/run/editor.ts @@ -24,9 +24,7 @@ export const editor = new Command() const project = await createProject(enginePath, projectPath) - console.log(compile) if (compile) { - console.log('compiling') await project.compile({ target: EngineTarget.Editor, configuration: configuration as EngineConfiguration, @@ -34,6 +32,5 @@ export const editor = new Command() }) } - console.log('running') await project.runEditor({ extraArgs: [...runArguments] }) }) diff --git a/src/lib/project.ts b/src/lib/project.ts index 00bf2b8..c75f5f2 100644 --- a/src/lib/project.ts +++ b/src/lib/project.ts @@ -98,10 +98,8 @@ export class Project { if (projectData && projectData.Plugins) { for (const plugin of projectData.Plugins) { if (plugin.Name === pluginName) { - console.log('found plugin') foundPlugin = true if (plugin.Enabled == shouldEnable) { - console.log(`plugin was already ${shouldEnable}d. Exiting.`) Deno.exit() } plugin.Enabled = shouldEnable @@ -109,7 +107,7 @@ export class Project { } } if (!foundPlugin) { - console.log(`could not find ${pluginName} in project plugin list, adding new entry`) + console.log(`Could not find ${pluginName} in project plugin list, adding new entry`) const newPlugin: UnrealEnginePluginReference = { Name: pluginName, Enabled: shouldEnable, @@ -118,7 +116,7 @@ export class Project { } writeUProjectFile(this.projectFileVars.projectFullPath, projectData as UProject) } else { - console.log('failed to parse project file') + console.log('Failed to parse project file') } }