Essay Evergreen

Execute Console Command is a development tool, not a runtime API

Execute Console Command is declared in the Development category, but unlike Print String it carries no DevelopmentOnly flag, so nothing in a graph marks it as a development tool. In runtime code it writes every console variable at ECVF_SetByConsole, above scalability, user settings, Project Settings, device profiles and hotfixes. It reports no failure to the graph that called it, and the same string resolves differently in the editor and in Shipping.

Table of contents

Execute Console Command is a development tool, and its declaration says so: the function sits in Category="Development". In Blueprint that category shows up as the node’s folder in the action menu and nowhere on the node once it is in a graph. In a project’s runtime code the node is the wrong way to change engine state. It writes every console variable (CVar) with the priority reserved for a developer typing at the console. It reports no failure to the graph that called it. And the same string can mean different things in the editor and in a Shipping build.

From inside a graph the node looks like an ordinary setter. It takes effect immediately in PIE, and it is the only way to write a CVar through the engine’s general Blueprint library: UKismetSystemLibrary provides CVar getters but no dedicated CVar setter (KismetSystemLibrary.h:609-636).

Event BeginPlay calls Execute Console Command with r.LightFunctionQuality 2. It compiles, runs in PIE, and changes nothing on screen.

What the node is

The declaration (Engine/Classes/Kismet/KismetSystemLibrary.h:593-600, UE 5.7):

/**
 * Executes a console command, optionally on a specific controller
 * 
 * @param   Command         Command to send to the console
 * @param   SpecificPlayer  If specified, the console command will be routed through the specified player
 */
UFUNCTION(BlueprintCallable, Category="Development",meta=(WorldContext="WorldContextObject", CallableWithoutWorldContext))
static ENGINE_API void ExecuteConsoleCommand(const UObject* WorldContextObject, const FString& Command, class APlayerController* SpecificPlayer = NULL );

The return type is void, and the comment says nothing about priorities, validation, or build configurations, because the function leaves all of them to the console code it calls.

The engine has a more explicit way to mark development nodes, and uses it in the same category. Print String is also declared in Development, but with DevelopmentOnly in its metadata (KismetSystemLibrary.h:561). A node with that flag gets a Development Only banner in the graph, and the banner’s tooltip says the node is treated as disabled in Shipping and Test builds cooked from a commandlet (Editor/GraphEditor/Private/SGraphNode.cpp:1086-1088). Execute Console Command has no such flag. In a graph it looks like any gameplay node, and it runs in Shipping.

Internally the node hands the string to IConsoleManager::ProcessUserConsoleInput, the function that handles a line typed into the console, and falls back to the player controller and GEngine->Exec when that function does not recognize it (Engine/Private/KismetSystemLibrary.cpp:547-567). In effect it types into the console on the graph’s behalf.

It writes as the developer

When the string names a CVar, ProcessUserConsoleInput sets it at a fixed priority (Core/Private/HAL/ConsoleManager.cpp:3076):

CVar->Set(*Param2, ECVF_SetByConsole);

Every CVar stores the priority of its last successful write, and a new write lands only if its priority is the same or higher (NewPri >= OldPri, ConsoleManager.cpp:272). The enum lists the priorities from lowest to highest (Core/Public/HAL/IConsoleManager.h:142-175). The table below shows the ones used to configure a shipped game, plus the one the node uses:

PriorityValueWritten by
Scalability0x02quality levels from Scalability.ini
GameSetting0x03UGameUserSettings
ProjectSetting0x04Project Settings
SystemSettingsIni0x05[ConsoleVariables] in Engine.ini
DeviceProfile0x07DeviceProfiles.ini
Hotfix0x0Bhotfixes
Console0x0Fthe editor UI and the interactive console

Console is the highest value in the enum. After one call to the node, every layer in the table loses to the console value, and none of them has a code path that removes the console entry again.

The engine’s own settings code writes with an explicit, lower priority. This is how UGameUserSettings applies VSync (Engine/Private/GameUserSettings.cpp:460-461):

static auto CVar = IConsoleManager::Get().FindConsoleVariable(TEXT("r.VSync"));
CVar->Set(IsVSyncEnabled(), ECVF_SetByGameSetting);

Quality levels go through Scalability::SetQualityLevels, which writes each group at ECVF_SetByScalability (Engine/Private/Scalability.cpp:446). The low position is deliberate. The enum comment says scalability is “lower priority than game settings so it’s easier to override partially” (IConsoleManager.h:148). The layers are meant to override each other in a fixed order, with the console on top for the developer. A runtime Execute Console Command puts shipped code in the developer’s seat.

A graph that sends r.VSync 0 through the node makes the player’s VSync setting ineffective for the rest of the session, because UGameUserSettings writes at GameSetting and loses to the console.

The case that prompted this post

This post came out of a project where a third-party plugin sets r.LightFunctionQuality with a console command from its Blueprint, while the game’s graphics menu is built on the engine quality levels. Every time the player applies the quality settings, the log gets this line:

LogConsoleManager: Warning: Setting the console variable 'r.LightFunctionQuality' with 'SetByScalability' was ignored as it is lower priority than the previous 'SetByConsole'. Value remains '2'

Typing the CVar name into the console without a value prints its history (ConsoleManager.cpp:3093-3095). With quality on Low:

HISTORY
Constructor: 2
Scalability: 0
Console: 2
r.LightFunctionQuality = "2"      LastSetBy: Console

The quality settings do their job. Set records the history before it checks the priority (ConsoleManager.cpp:1342 and :1347), so the scalability write is there, and on Cinematic its line reads Scalability: 1. Console: 2 outranks it either way. That 2 is also the constructor value: the plugin writes the engine default back.

The engine reads this CVar in one place, and the check is <= 0 (Engine/Private/ShowFlags.cpp:462):

static const auto ICVar = IConsoleManager::Get().FindTConsoleVariableDataInt(TEXT("r.LightFunctionQuality"));
if(ICVar->GetValueOnGameThread() <= 0)
{
    EngineShowFlags.SetLightFunctions(false);
}

Medium and higher quality levels request 1, which behaves exactly like 2. Low requests 0 (Engine/Config/BaseScalability.ini:133), which switches light functions off, and that is the value the console write blocks. The override only has a cost on Low, the quality level a player picks to reduce frame time.

It reports nothing

The node returns void, so the graph cannot branch on the result. When the call itself fails, the failure goes to the log:

  • A misspelled name: ProcessUserConsoleInput finds no CVar and returns false (ConsoleManager.cpp:2968-2972). The string falls through to the player controller, which prints Command not recognized: … (Engine/Private/Player.cpp:58).
  • A read-only CVar: the console prints Error: … is read only! (ConsoleManager.cpp:3071) and writes nothing.

Nothing checks the string at compile time either. Command is an FString pin, and neither the C++ compiler nor the Blueprint compiler knows that it names a CVar, so a typo compiles as cleanly as a correct name.

A call that succeeds is harder to trace. Its effect shows up when the quality settings or UGameUserSettings later try to write the same CVar and lose. The warning then appears on the settings side, in a different system and often long after the call that caused it.

In a default Shipping build none of these lines are printed, because NO_LOGGING is defined as !USE_LOGGING_IN_SHIPPING (Core/Public/Misc/Build.h:351). The failures still happen.

Its meaning depends on the build

PIE runs an editor build, and a packaged game resolves the same string differently.

Cheat CVars are switched off. DISABLE_CHEAT_CVARS is defined as (UE_BUILD_SHIPPING || (UE_BUILD_TEST && !ALLOW_CHEAT_CVARS_IN_TEST)) (Core/Public/Misc/Build.h:440), and under it ProcessUserConsoleInput returns false for any CVar flagged ECVF_Cheat without touching the value (ConsoleManager.cpp:2974-2977). The same node with the same string sets the CVar in PIE and does nothing in Shipping.

Development commands are compiled out. When the string names a command rather than a CVar, Shipping may not have its handler: UEngine::Exec_Dev wraps its whole body in #if !UE_BUILD_SHIPPING (Engine/Private/UnrealEngine.cpp:5450), so a command that works in PIE has no handler in the shipped game.

The commands that could fix the priority are developer tools too. SetCVar … -setby= and UnsetCVar let a string pick a priority or remove one, but they are compiled only under #if ALLOW_OTHER_PLATFORM_CONFIG (ConsoleManager.cpp:2831). That macro is WITH_UNREAL_DEVELOPER_TOOLS, and Test and Shipping targets do not build developer tools by default.

A runtime code path whose meaning depends on the build configuration cannot be verified in the editor.

Where it is the right tool

The priority that makes the node harmful in runtime code is what a developer wants from a debug tool:

  • debug menus and cheat panels, where overriding every layer is the point,
  • stat overlays, visualizers, and profiling switches,
  • automation and functional tests that need to force a state,
  • Editor Utility Widgets and other editor-only tooling.

In all of these the caller is the developer, so the console priority matches who is writing. The boundary is whether the call can reach a player. A debug widget that ends up in a Shipping build has every problem of runtime code, plus cheat CVars that stop working without an error.

Blueprint-only projects

The strongest counterargument is that, in a Blueprint-only project, the node is the only way to write an arbitrary CVar. The settings a player normally controls do not need it. UGameUserSettings exposes them to Blueprint with the engine’s priorities behind them: ApplySettings, SetFrameRateLimit, SetShadowQuality, and the other quality setters are all BlueprintCallable (Engine/Classes/GameFramework/GameUserSettings.h:49, :143, :186).

A CVar that no engine system manages needs one small C++ function. The priority enum is not a UENUM, so a BlueprintCallable wrapper picks the priority and calls this:

bool SetCVarInt(const TCHAR* Name, int32 Value, EConsoleVariableFlags SetBy)
{
    IConsoleVariable* CVar = IConsoleManager::Get().FindConsoleVariable(Name);
    if (!CVar)
    {
        return false;
    }
    CVar->Set(Value, SetBy);
    return (CVar->GetFlags() & ECVF_SetByMask) == SetBy;
}

Set returns void, but a write that lands also stores its priority on the CVar (ConsoleManager.cpp:320), so reading the priority back tells the caller whether the write took effect. The function reports a missing CVar too, and it never goes through command routing, so no exec handler can disappear between PIE and Shipping.

What to use instead

Each value of the priority enum carries a comment that names the code it is meant for (IConsoleManager.h:144-175), and each layer a shipped game needs has an owner that writes at its own level:

  • the quality levels belong to the scalability system, which is exposed to Blueprint through UGameUserSettings (Scalability),
  • the player’s own settings belong to UGameUserSettings (GameSetting),
  • project-wide defaults belong to Project Settings and the project’s ini files (ProjectSetting, SystemSettingsIni),
  • per-platform values belong to device profiles (DeviceProfile).

Project Settings and the ini files outrank the quality levels. A value that should differ per quality level belongs in the project’s scalability ini, next to the engine’s own sections such as [ShadowQuality@0], and not in Project Settings or [ConsoleVariables].

Console is described as the priority used via the editor UI or the interactive console (IConsoleManager.h:174), which means a person at a keyboard. Runtime code should write at the priority whose description matches the caller, and the owners listed above already do.

Tip
To remove a console-priority override that a third-party plugin has applied, call IConsoleVariable::Unset(ECVF_SetByConsole) from C++. It removes one priority from the history and recomputes the value from what is left (ConsoleManager.cpp:1172, recompute at :1200-1216), and no build configuration guards it. In the case above the highest remaining entry is Scalability, so the value returns to whatever the quality level asks for. The plugin’s next console command reapplies the override, so the lasting fix is in the plugin.

Why it matters most in plugins

In a game’s own runtime graph, the node overrides the settings system the game itself depends on. In a plugin’s runtime graph, it does that in every project that installs it, outranking settings, device profiles, and hotfixes its author has never seen. The trace it leaves in each of those projects is a warning line each time a lower layer is overruled, and a Shipping build does not print it. A plugin that needs a particular value should document it and let the project put it at the right layer. If it has to force one, it should expose a switch that turns the override off.

See also

Related pages