Editor tools
An editor plugin’s library runs in the editor only, and binds the editor’s modules, which the editor template lists. It reaches the engine’s editor-only functions as well, such as set_actor_label.
Use case: a menu entry
The goal: Tools > LevelTools > Count assets runs Rust code.
A menu entry is a class whose parent is ToolMenuEntryScript, with execute overridden:
#![allow(unused)]
fn main() {
use bindings::tool_menus::{FToolMenuContext, ToolMenuEntryScript};
#[uclass(parent = ToolMenuEntryScript)]
pub struct CountAssetsEntry {}
#[uclass_impl]
impl CountAssetsEntry {
#[ufunction(Override)]
fn execute(&mut self, _context: UStructRef<FToolMenuContext>) {
count_assets("/Game");
}
}
}
An editor subsystem creates it when the editor starts and removes it when it closes:
#![allow(unused)]
fn main() {
use bindings::manual::world_ext::new_object;
use bindings::rusteal_editor::RustealEditorSubsystem;
use bindings::tool_menus::{ToolMenuEntryScript, ToolMenuEntryScriptExt};
#[uclass(parent = RustealEditorSubsystem)]
pub struct LevelToolsSubsystem {
#[uproperty]
menu_entry: UObjectRef<ToolMenuEntryScript>,
}
#[uclass_impl]
impl LevelToolsSubsystem {
#[ufunction(Override)]
fn receive_initialize(&mut self) {
if let Err(e) = self.add_menu_entry() {
log::warn!("[LevelTools] no menu entry: {e}");
}
}
#[ufunction(Override)]
fn receive_deinitialize(&mut self) {
if let Ok(entry) = self.menu_entry().checked() {
entry.unregister_menu_entry();
}
}
}
impl LevelToolsSubsystem {
fn add_menu_entry(&mut self) -> RustealResult<()> {
let entry = new_object::<CountAssetsEntry>(&self.as_ref())?.upcast_to::<ToolMenuEntryScript>();
let script = entry.checked()?;
script.init_entry(
FName::new("LevelTools").handle(),
FName::new("LevelEditor.MainMenu.Tools").handle(),
FName::new("LevelTools").handle(),
FName::new("LevelToolsCountAssets").handle(),
Some("Count assets"),
Some("Count the project's assets by type, off the game thread"),
);
script.register_menu_entry();
self.set_menu_entry(entry);
Ok(())
}
}
}
init_entry takes the owner, the menu (LevelEditor.MainMenu.Tools is the Tools menu), the section, the entry’s name, its label and its tooltip.
Use case: work on many assets, off the game thread
The goal: list the project’s assets and count them by name prefix without freezing the editor.
#![allow(unused)]
fn main() {
use bindings::unreal_ed::{EditorAssetSubsystem, EditorAssetSubsystemExt};
use rusteal_runtime::runtime::task;
pub fn count_assets(path: &str) {
let assets = match editor_subsystem::<EditorAssetSubsystem>().and_then(|s| s.checked()) {
Ok(subsystem) => subsystem.list_assets(path, Some(true), Some(false)),
Err(e) => {
log::error!("[LevelTools] no asset subsystem: {e}");
return;
}
};
let path = path.to_string();
task::spawn(
move || {
let mut by_prefix = std::collections::BTreeMap::<String, usize>::new();
for asset in &assets {
let name = asset.rsplit('/').next().unwrap_or(asset);
let prefix = name.split_once('_').map_or("(no prefix)", |(p, _)| p);
*by_prefix.entry(prefix.to_string()).or_default() += 1;
}
(assets.len(), by_prefix)
},
move |(total, by_prefix)| {
log::info!("[LevelTools] {total} assets under {path}");
for (prefix, count) in by_prefix {
log::info!("[LevelTools] {prefix}: {count}");
}
},
);
}
}
The engine call that lists the assets runs on the game thread; the counting, plain Rust, runs on a thread of its own (see Work off the game thread).
The editor’s own subsystems (EditorActorSubsystem, EditorAssetSubsystem, LevelEditorSubsystem) come from EditorSubsystemBlueprintLibrary::get_editor_subsystem, which the template wraps:
#![allow(unused)]
fn main() {
use bindings::editor_subsystem::EditorSubsystem;
use bindings::unreal_ed::EditorSubsystemBlueprintLibrary;
use rusteal_runtime::runtime::{Inherits, RustealResult, SubclassOf, UObjectRef, UeClass};
pub fn editor_subsystem<T: UeClass + Inherits<EditorSubsystem>>() -> RustealResult<UObjectRef<T>> {
let class = SubclassOf::<T>::base().upcast_to::<EditorSubsystem>();
EditorSubsystemBlueprintLibrary::get_editor_subsystem(class).cast::<T>()
}
}
SubclassOf::<T>::base() is T’s class, and upcast_to passes it where a parent’s class is expected; the Inherits bound lets the compiler check that T is an editor subsystem.
Use case: validating assets on save
The goal: saving an asset whose name has no type prefix (SM_, T_, BP_) fails, with a message.
#![allow(unused)]
fn main() {
use bindings::core_ue::Object;
use bindings::data_validation::{EditorValidatorBase, EditorValidatorBaseExt};
const INVALID: u8 = 0;
const VALID: u8 = 1;
#[uclass(parent = EditorValidatorBase)]
pub struct NameValidator {}
#[uclass_impl]
impl NameValidator {
#[ufunction(Override, name = "K2_CanValidateAsset")]
fn can_validate_asset(&self, asset: UObjectRef<Object>) -> bool {
asset.is_valid()
}
#[ufunction(Override, name = "K2_ValidateLoadedAsset")]
fn validate_loaded_asset(&mut self, asset: UObjectRef<Object>) -> u8 {
let Ok(name) = asset.get_name() else {
return VALID;
};
let Ok(me) = self.as_ref().checked() else {
return VALID;
};
if name.contains('_') {
me.asset_passes(asset);
VALID
} else {
me.asset_fails(asset, &format!("'{name}' has no type prefix (SM_, T_, BP_...)"));
INVALID
}
}
}
}
A class whose parent is EditorValidatorBase runs when assets are saved, and on Validate Assets in the Content Browser’s right-click menu. It returns an EDataValidationResult as its u8 value. Validation results show in the Message Log.
Use case: one undo step for many changes
KismetSystemLibrary::begin_transaction, transact_object and end_transaction group changes into one step of Edit > Undo: begin the transaction, call transact_object on each object before changing it, and end the transaction.