---
name: asset-management
display_name: Asset Discovery & Management
description: Import files from disk (images, meshes, any format the editor imports), export textures, query the Content Browser selection, and check if an asset is open (AssetDiscoveryService). Search, load, save, move, rename, duplicate, and delete assets are handled by Unreal's native AssetTools toolset or EditorAssetLibrary. Use when the user asks to import an image, mesh or other file from disk, export a texture, query the Content Browser selection, or check whether an asset is open in an editor.
  Also use for static-mesh LOD reimport, section material mapping, or gray/default surfaces after reimport.
vibeue_classes:
  - AssetDiscoveryService
unreal_classes:
  - EditorAssetLibrary
  - AssetRegistryHelpers
---

> 🧠 **Brains complement:** IF an `unreal-engine-skills-manager` tool (external MCP) exists in this session, call it with `{action: "load", skill: "asset-management"}` for UE domain knowledge on this topic — correct APIs, architecture, best practices — and treat it as the rubric for any review / "best practices" question. If no such tool is available (e.g. running under Claude Code or Codex without that MCP), skip this line entirely and proceed with this skill alone — do NOT attempt the call.

# Asset Discovery & Management Skill

For static-mesh LOD reimport, lost textures or gray/default surfaces despite a
correct component material, read [mesh reimport material mappings](references/mesh-reimport-materials.md).
The bundled `scripts/mesh_material_slots.py` exposes explicit section inspection
and remapping through the existing engine Python API; no native service rebuild.

> 🔀 **Engine owns general asset ops now.** In the Unreal 5.8 consolidation, searching, loading,
> saving, moving, renaming, duplicating, and deleting assets moved to Unreal's native **`AssetTools`**
> toolset (reach it via `call_tool`; run `describe_toolset` for its actions) — or you can drive
> `unreal.EditorAssetLibrary` / `unreal.AssetRegistryHelpers` directly from `execute_python_code`.
> The old **`manage_asset` MCP tool is GONE.** VibeUE's `AssetDiscoveryService` was trimmed to only
> the crash-safe delta the engine doesn't provide:
>
> | Kept on `AssetDiscoveryService` | Purpose |
> |---|---|
> | `import_asset(src, dest_folder, name)` → `(path, err)` | Import an image, mesh or any importable file (folder + name) |
> | `import_texture(src, dest_asset_path)` | Same importer, full asset path |
> | `export_texture(asset_path, file_path)` | Export a texture to disk |
> | `get_primary_content_browser_selection()` → `AssetData or None` | Primary Content Browser selection |
> | `is_asset_open(asset_path)` → `bool` | Whether an asset is open in an editor |
>
> Everything else below (`search_assets`, `list_assets_in_path`, `move_asset`, `duplicate_asset`,
> `delete_asset`, `save_asset`, `get_open_assets`, `get_asset_referencers`, `find_asset_by_path`,
> `asset_exists`, …) was **removed** from `AssetDiscoveryService` — use the engine `AssetTools`
> toolset or `EditorAssetLibrary` / the Asset Registry.

## Data Assets & Data Tables — no dedicated VibeUE service (issues #451, #452)

There is **no `DataAssetService` / `DataTableService`**. Drive them natively:

```python
import unreal

# --- Data Assets (UPrimaryDataAsset / UDataAsset subclasses) ---
# Create: engine DataAssetTools toolset via call_tool (describe_toolset for the action), OR a factory.
da = unreal.load_asset("/Game/Data/DA_Thing")
val = da.get_editor_property("MyField")          # read
da.set_editor_property("MyField", 42)            # write — works for any UPROPERTY
unreal.EditorAssetLibrary.save_loaded_asset(da)

# --- Data Tables ---
dt = unreal.load_asset("/Game/Data/DT_Items")
names = unreal.DataTableFunctionLibrary.get_data_table_row_names(dt)   # row keys
# Read/write rows with the editor DataTable API:
#   unreal.DataTableFunctionLibrary.evaluate_curve_table_row(...) / get_data_table_column_as_string(dt, "Col")
# Engine DataTableTools (call_tool) covers add/remove/get rows + import/export CSV/JSON.
```

> ⚠️ **Data Table gotchas (verified, #452):** the row **key** ("Name") is reported as a column by
> some schema readers — it is the row id, not a data field. Writing a row with the wrong value type
> can be silently coerced — read the row back to confirm. There is no `clear_rows`; remove rows
> individually (engine `DataTableTools`) or re-import an empty CSV. An empty `GameplayTag` field
> serializes as `"None"`.

## Critical Rules

### ⚠️ `delete_asset` on a REFERENCED asset opens a modal dialog that wedges an unattended editor

`EditorAssetLibrary.delete_asset` asks "this asset is referenced, force delete?" through a modal
window when anything points at the asset (a data asset holding the montage you are replacing, a
Blueprint default, a level). Nobody answers it from MCP: the game thread stalls (308 s observed),
every later call hangs, and the process has to be force-killed with the files removed from disk.
Use the plugin's dialog-free delete instead:

```python
# Refuses when referenced and tells you who:
result = unreal.AssetDiscoveryService.delete_asset_unattended("/Game/Anim/AM_Old", False)
# Delete anyway and null every reference (what the dialog's Force Delete does):
result = unreal.AssetDiscoveryService.delete_asset_unattended("/Game/Anim/AM_Old", True)
if not result.b_success:
    print(result.error_message)     # why it did not happen
    print(result.referencers)       # what pointed at the asset
```

The call returns an `FUnattendedDeleteResult` struct — `result.b_success`, `result.referencers`,
`result.error_message` — so the reason ALWAYS survives, even on a refusal. (It used to return a
bool with out-params, and Python maps a `False` return to `None`, which silently dropped the reason;
that is fixed.) `result.referencers` is filled on a refusal AND on a forced delete, so you always see
what pointed at the asset. Prefer creating the replacement under a NEW name and repointing references
over force-deleting.

**Even `delete_asset_unattended(path, True)` refuses a natively rooted in-memory object — it never
prompts.** Before deleting, the call does a conservative, non-mutating check: it looks at who holds the
asset and refuses ONLY when a native GC root holds it directly — a `UGCObjectReferencer`, which in an
agent session is a Python module-level global that created or loaded the asset earlier this session.
That is the one case the engine's own force delete cannot clear, so it would stall on the modal "is in
use" dialog (6+ minutes observed). When it refuses, the call returns `b_success == False` with those
roots in `result.referencers` and an `result.error_message` explaining it. The fix is what the error
says: release the Python globals holding it — `del my_var`, then `unreal.SystemLibrary.collect_garbage()`
— and retry. Every OTHER in-memory referencer is left to the engine's real force delete, which clears it
without prompting: on-disk asset references, the Blueprint-palette node spawners, and transient editor
helpers like an anim data controller all delete fine. Read-only package files are also refused before
the engine delete path, because Unreal can show a read-only-package prompt even when confirmation is
disabled. Clear the filesystem/source-control read-only state explicitly, then retry.

The check never mutates state (it does not null any references before deciding), so a refusal leaves the
asset and everything around it exactly as they were — safe to retry after releasing the global.

### ⚠️ Never `delete_asset` a Blueprint you loaded or compiled this session

`EditorAssetLibrary.delete_asset` on a Blueprint (Anim Blueprints especially) that is still loaded —
which it is, if you created, compiled or read it earlier in the same script — fails inside
`ForceDeleteObjects` and leaves the package **half-deleted and corrupt**:

```
Ensure condition failed: false [ObjectTools.cpp:4045]
Failed to unload all packages during ForceDeleteObjects - these packages are likely corrupt.
Consider restarting the editor, noting which assets remain and then deleting them from the
file system manually: /Game/Path/ABP_Thing
```

Every later call against that path then times out, and the editor typically has to be killed. The
recovery is exactly what the message says — close the editor, delete the `.uasset` from disk,
relaunch (watch for a ZenServer stall on the way back up) — so it costs several minutes.

**The rebuild-an-asset pattern**, instead of delete-then-create:

- Write to a **new name** and swap references, or
- Delete the file on disk while the editor is closed, then create it fresh, or
- Edit the existing asset in place (clear the graph, re-add nodes) rather than recreating it.

The same caution applies to any asset currently open in an editor tab or referenced by a loaded
level. Non-Blueprint assets you never loaded (textures, meshes written by a factory) delete fine.

### ⚠️ Out-Params Become Return Values in Python — Never Pass an `AssetData` Argument

`get_primary_content_browser_selection` is shaped like `bool Func(FAssetData& Out)` in C++ and is
exposed to Python as `() -> AssetData or None`. Passing an `AssetData` argument raises `TypeError`.

```python
# WRONG - TypeError: takes no arguments (1 given)
asset = unreal.AssetData()
unreal.AssetDiscoveryService.get_primary_content_browser_selection(asset)

# CORRECT - call with no out-arg, check the return for None
asset = unreal.AssetDiscoveryService.get_primary_content_browser_selection()
if asset:
    print(asset.asset_name)
```

### 🔀 Where each operation lives now

| Operation | Use |
|-----------|-----|
| Search / find / list assets | engine **`AssetTools`** toolset via `call_tool`, or `unreal.AssetRegistryHelpers.get_asset_registry()` |
| Load / save / save-all | `unreal.EditorAssetLibrary.load_asset` / `save_asset` / `save_directory`, or `AssetTools` |
| Move / rename / duplicate / delete | `unreal.EditorAssetLibrary.rename_asset` (move), `duplicate_asset`, `delete_asset` (unreferenced only — see the modal warning above), or `AssetTools`; `unreal.AssetDiscoveryService.delete_asset_unattended` for anything that may be referenced |
| Existence check | `unreal.EditorAssetLibrary.does_asset_exist(path)` |
| Referencers / dependencies | `unreal.AssetRegistryHelpers.get_asset_registry().get_referencers(...)` |
| Open an asset / list ALL open editors | Epic `EditorAppToolset` via `call_tool` (see below) |
| Import a file from disk (image, mesh, …) | `unreal.AssetDiscoveryService.import_asset` / `import_texture` (**see below**) |
| Export a texture to disk | `unreal.AssetDiscoveryService.export_texture` |
| Primary Content Browser selection | `unreal.AssetDiscoveryService.get_primary_content_browser_selection()` |
| Is an asset open in an editor | `unreal.AssetDiscoveryService.is_asset_open(path)` |

### 🔀 ALL open assets / ALL selections / open an asset — use Epic's `EditorAppToolset`

VibeUE covers the **single/primary** queries above; the **list-all** and **open** operations live on
Epic's native `EditorToolset.EditorAppToolset` (call via `call_tool`; returns `{"returnValue": [...]}`
of package-path strings). These are NOT Python-bound — they only work through `call_tool`:

| Need | Call |
|---|---|
| All assets open in editors | `call_tool("GetOpenAssets", "EditorToolset.EditorAppToolset")` |
| All Content Browser selections | `call_tool("GetSelectedAssets", "EditorToolset.EditorAppToolset")` |
| Open an asset in its editor | `call_tool("OpenEditorForAsset", "EditorToolset.EditorAppToolset", {"assetPath": "/Game/.../BP_X"})` |

```python
# "Show me all open Blueprints" = Epic GetOpenAssets + a type filter
opens = call_tool("GetOpenAssets", "EditorToolset.EditorAppToolset")["returnValue"]
bps = [p for p in opens if isinstance(unreal.load_asset(p), unreal.Blueprint)]

# "Open the selected asset" = VibeUE primary-selection + Epic OpenEditorForAsset
sel = unreal.AssetDiscoveryService.get_primary_content_browser_selection()
if sel:
    call_tool("OpenEditorForAsset", "EditorToolset.EditorAppToolset",
              {"assetPath": str(sel.package_name)})
```

### 🚨 Never list broad paths — a `/Game` listing can return 30,000+ assets

Listing every asset under `/Game` returns tens of thousands of entries and floods the conversation.
Whether you go through the engine `AssetTools` toolset or `EditorAssetLibrary.list_assets`, always
narrow first:

- Filter by a **specific subfolder** (e.g. `/Game/Blueprints/HUD`), and by class where supported.
- If you only need a count or sample, slice in Python before printing — never print the full array.

### ⚠️ `EditorAssetLibrary` Save Methods

There is **no `save_dirty_assets`** (`AttributeError`). The real options:
`unreal.EditorAssetLibrary.save_asset(path)`, `save_directory(dir)`, `save_loaded_asset(obj)`,
`save_loaded_assets([objs])`. To save everything dirty, iterate or use the engine `AssetTools`
toolset's save action.

### ⚠️ `EditorAssetLibrary.list_assets` Has No Class Filter

`unreal.EditorAssetLibrary.list_assets(directory_path, recursive=True, include_folder=False)`
returns **path strings only** — there is no `asset_class_names` or any other class-filter kwarg
(`TypeError: invalid keyword argument`). To list assets of one type, either filter the returned
paths in Python (load each and check its class), or query the Asset Registry with a class filter
via `unreal.AssetRegistryHelpers.get_asset_registry().get_assets(...)`.

### ⚠️ UE 5.8 `AssetData` Property Names

`get_primary_content_browser_selection()` returns an `AssetData`. Read it with:

| WRONG (old) | CORRECT |
|-------------|---------|
| `asset.name` | `asset.asset_name` |
| `asset.path` | `asset.package_path` |
| `asset.asset_class` | `asset.asset_class_path` (a `TopLevelAssetPath`; compare `.asset_name`) |
| `asset.object_path` | `f"{asset.package_name}.{asset.asset_name}"` |

`AssetData` has **no** `object_path` attribute (`AttributeError`). Build it from `package_name` +
`asset_name`, or just use `str(asset.package_name)`.

### ⚠️ Importing Files From Disk — Use `AssetDiscoveryService.import_asset`

To bring a file from disk into the Content Browser, use **`AssetDiscoveryService.import_asset`**
(or `import_texture`, which takes a full asset path and runs the same importer). It takes any
format the editor imports, and saves what it imports:

- **Images** (png, jpg, jpeg, bmp, tga, dds, exr, hdr, tiff, tif, psd, pcx) go through the
  texture factory's direct binary path and become a `Texture2D`.
- **Everything else** (FBX, OBJ, glTF/GLB, audio, …) goes through an automated, synchronous
  `AssetImportTask` — no option dialogs.

```python
import unreal

# Disk → Content Browser. Returns (asset_path, error); path is "" on failure.
path, err = unreal.AssetDiscoveryService.import_asset(
    "C:/Images/rocks.jpg", "/Game/UI/Textures", "T_Rocks")
print(path or err)

# A mesh takes the same call. Give a file that brings materials/textures a folder of its own.
path, err = unreal.AssetDiscoveryService.import_asset(
    "C:/Art/crate.fbx", "/Game/Props/Crate", "SM_Crate")
print(path or err)
```

What to expect:

- **Same-name assets are replaced, without a prompt.** That includes the side assets a
  multi-asset file brings along: an FBX's materials and textures (and a skeletal mesh's Skeleton
  and PhysicsAsset) overwrite any asset of the same name already in the destination folder. Their
  names are only known once the importer has run, so there is no pre-check — import such files
  into an empty or dedicated folder.
- **The returned path is the main asset.** For a file that brings several assets it is the mesh,
  not a material or texture; when the importer did not use the name you passed, the path carries
  the importer's name. List the folder (Asset Registry) to find the side assets.
- **The one context where an import would crash, and is refused instead.** A synchronous AssetTools
  import waits by pumping the game thread's task queue. If the caller is itself running *inside* a
  game-thread task (the queue is already being processed), TaskGraph's `RecursionGuard` asserts and
  the editor goes down. `import_asset` checks for exactly that and returns an error naming
  `RecursionGuard` instead. `execute_python_code` and MCP tool calls do not run inside such a task,
  so imports from them go ahead. Calling `unreal.AssetToolsHelpers.get_asset_tools().import_asset_tasks(...)`
  yourself skips that check (and the save, and the main-asset pick) — prefer `import_asset`.

Need a source image to import? Editor screenshots live under the **project's**
`Saved/Screenshots` (and `Saved/VibeUE/Screenshots`):

```python
import os, unreal
shots = os.path.join(unreal.Paths.project_saved_dir(), "Screenshots")
print(os.listdir(shots) if os.path.isdir(shots) else "no screenshots yet")
```

### ⚠️ Asset Paths Must Be Content Browser Paths

Use `/Game/...` paths, **not** file system paths, everywhere except the disk side of import/export.

```python
# WRONG - file system path
unreal.EditorAssetLibrary.does_asset_exist("C:/Projects/Content/BP_Player.uasset")

# CORRECT - content browser path
unreal.EditorAssetLibrary.does_asset_exist("/Game/Blueprints/BP_Player")
```

### ⚠️ Never Emulate Move/Rename with Duplicate + Delete

Duplicating creates a new asset identity. References stay pointed at the original, so deleting the
original after a duplicate can break those references. Use a real move/rename (which fixes up
references) instead:

```python
import unreal

# EditorAssetLibrary.rename_asset performs a move + reference fixup
unreal.EditorAssetLibrary.rename_asset(
    "/Game/StateTree/STT_Rotate",
    "/Game/StateTree/Tasks/STT_Rotate")
```

(The engine `AssetTools` toolset exposes an equivalent move/rename action via `call_tool`.)

### Creating Widget Blueprints

Use the engine's `WidgetBlueprintFactory` — a plain `BlueprintFactory` with a UserWidget
parent creates a regular Blueprint, not a `WidgetBlueprint` (the removed
`BlueprintService.create_blueprint` used to pick the UMG factory automatically):

```python
factory = unreal.WidgetBlueprintFactory()
factory.set_editor_property("ParentClass", unreal.UserWidget)
wbp = unreal.AssetToolsHelpers.get_asset_tools().create_asset("WBP_Menu", "/Game/UI", None, factory)
```

For designing the widget afterwards (hierarchy, slots, bindings), load the **umg-widgets** skill.

---

## Workflows

### Search Pattern

Searching is an engine **`AssetTools`** job (via `call_tool`) — or query the Asset Registry directly:

```python
import unreal

ar = unreal.AssetRegistryHelpers.get_asset_registry()

# All Blueprints under /Game (recursive). Use a class filter and a narrow path to avoid huge results.
f = unreal.ARFilter(
    package_paths=["/Game/Blueprints"],
    class_paths=[unreal.TopLevelAssetPath("/Script/Engine", "Blueprint")],
    recursive_paths=True)
for a in ar.get_assets(f)[:25]:
    print(f"{a.asset_name}: {a.package_path}")
```

### Check Exists Pattern

```python
import unreal

if unreal.EditorAssetLibrary.does_asset_exist("/Game/BP_Player"):
    print("Found")
else:
    print("Not found - create it")
```

### Duplicate Pattern

```python
import unreal

dup = unreal.EditorAssetLibrary.duplicate_asset("/Game/BP_Enemy", "/Game/BP_EnemyBoss")
if dup:
    print("Duplicated")
```

### Move / Rename Pattern

```python
import unreal

ok = unreal.EditorAssetLibrary.rename_asset(
    "/Game/StateTree/STT_Rotate",
    "/Game/StateTree/Tasks/STT_Rotate")
print("Moved (references fixed up)" if ok else "Move failed")
```

### Save Pattern

```python
import unreal

# Save a specific asset
unreal.EditorAssetLibrary.save_asset("/Game/BP_Player")

# Save a whole directory of dirty assets
unreal.EditorAssetLibrary.save_directory("/Game/Blueprints")
```

### Check References Before Delete

Asset Registry referencer queries return **package-name strings**, not `AssetData`.

```python
import unreal

ar = unreal.AssetRegistryHelpers.get_asset_registry()
refs = ar.get_referencers(
    "/Game/SM_Weapon",
    unreal.AssetRegistryDependencyOptions(include_hard_package_references=True))
if not refs:
    unreal.EditorAssetLibrary.delete_asset("/Game/SM_Weapon")
else:
    for ref in refs:          # ref is a package name, e.g. "/Game/Blueprints/BP_Player"
        print(f"In use by: {ref}")
```

> ⚠️ **Deleting levels:** `delete_asset` on a recently-loaded `.umap` can return `True` (asset gone
> from the registry) yet **leave the package file on disk**, so `delete_directory` on its folder then
> fails. After deleting level assets, verify the `Content/...` folder on disk and remove leftover
> `.umap` files manually.

> ⚠️ **`delete_asset` false success is not limited to levels (issue #557).** Blueprints with live
> references have also returned `True` while their files stayed on disk — and later touching such
> half-deleted assets ("files gone, editor memory ghosts") has crashed the editor. After ANY
> scripted delete, verify with a filesystem check (`os.path.exists` on the `.uasset`) or
> `does_asset_exist`, and never read properties (e.g. mesh bounds) off an asset you just deleted.

> ℹ️ **`CaptureAssetImage` cannot render Blueprints or Niagara systems** ("Asset type does not
> support image capture"). To preview those, spawn them in the level on a clear spot, aim the
> viewport camera, and use the `capture_image` MCP tool; delete the preview actors after.

### Import / Export Textures (VibeUE)

```python
import unreal

# Import (disk → Content Browser). Returns (asset_path, error); asset_path is "" on failure.
# Pass a destination FOLDER + optional asset name.
path, err = unreal.AssetDiscoveryService.import_asset(
    "C:/Textures/logo.png", "/Game/Textures", "T_Logo")
print(path or err)

# import_texture(src, dest_asset_path) takes a full asset path and uses the same importer:
unreal.AssetDiscoveryService.import_texture("C:/Textures/logo.png", "/Game/Textures/T_Logo")

# Export (project → file system)
unreal.AssetDiscoveryService.export_texture("/Game/Textures/T_Logo", "C:/Exports/logo.png")
```

### Editor State & Content Browser (VibeUE)

```python
import unreal

# Is a specific asset open in an editor?
if unreal.AssetDiscoveryService.is_asset_open("/Game/BP_Player"):
    print("BP_Player is open")

# Primary Content Browser selection — NO out-arg, returns AssetData or None
asset = unreal.AssetDiscoveryService.get_primary_content_browser_selection()
if asset:
    print(f"Selected: {asset.asset_name} at {asset.package_path}")
    # Open it via the AssetEditorSubsystem (engine), loading the object first:
    obj = unreal.EditorAssetLibrary.load_asset(str(asset.package_name))
    unreal.get_editor_subsystem(unreal.AssetEditorSubsystem).open_editor_for_assets([obj])
else:
    print("Nothing selected")
```

> For the full multi-selection set, open-editor lists, and search, use the engine `AssetTools` /
> `AssetEditorSubsystem` toolsets via `call_tool`.

### Inspect the Selection's Class

`asset.asset_class_path` is a `TopLevelAssetPath` struct, not a string — compare its `asset_name`:

```python
import unreal

asset = unreal.AssetDiscoveryService.get_primary_content_browser_selection()
if asset and str(asset.asset_class_path.asset_name) == "Blueprint":
    print(f"{asset.asset_name} is a Blueprint")
```

### Create Non-Standard Asset Types (Factory Pattern)

Assets not covered by a VibeUE service (e.g., `LandscapeGrassType`) require `AssetToolsHelpers` + a
factory. (`create_asset` imports nothing, so the import caveats above do not apply to it.)

```python
import unreal

asset_tools = unreal.AssetToolsHelpers.get_asset_tools()

factory = unreal.LandscapeGrassTypeFactory()
lgt = asset_tools.create_asset("LGT_MyGrass", "/Game/Landscape", unreal.LandscapeGrassType, factory)

# Set properties via set_editor_property, then save
unreal.EditorAssetLibrary.save_asset("/Game/Landscape/LGT_MyGrass")
```

> **Tip:** Use `discover_python_module("unreal", name_filter="Factory")` to find available factories.

### Common Asset Class Names for Filtering

- `Blueprint`, `WidgetBlueprint`
- `Texture2D`, `Material`, `MaterialInstanceConstant`
- `StaticMesh`, `SkeletalMesh`
- `DataTable`, `PrimaryDataAsset`
- `LandscapeGrassType`, `LandscapeLayerInfoObject`

Class names must match UE class names exactly (e.g., `LandscapeGrassType`, not `GrassType`). When a
typed Asset Registry query unexpectedly returns 0, retry without the class filter and inspect each
result's `asset_class_path.asset_name` to learn the real class name.

## Sample scripts (run via `execute_python_code`)

- **`scripts/find_and_save.txt`** — find an asset (Asset Registry / `EditorAssetLibrary`) and duplicate + save it.

## Additional gotchas

- `unreal.Rotator(...)` positional order is `(roll, pitch, yaw)` — pass by keyword.
- `unreal.AssetTools.create_asset(...)` is a descriptor and throws; the working call is `AssetToolsHelpers.get_asset_tools().create_asset(...)`.
- `EditDefaultsOnly` properties cannot be written on instances from Python; a server-only `UPROPERTY()` with no Blueprint flag reads as "protected"; and when a bool `bIsDead` and a `UFUNCTION IsDead()` both map to `is_dead`, the property wins.
- `TInstancedStruct` authoring: `inst = unreal.InstancedStruct(); inst.import_text('/Script/<Module>.<Struct>(Field=...)')`; a `TSoftClassPtr` UPROPERTY wants the loaded class, not a `SoftClassPath`.
- `LevelEditorPlaySettings` is not in the Python stub — reach it via `load_class(None, "/Script/UnrealEd.LevelEditorPlaySettings")` and exact CamelCase property names.
- Editor-world traces need a real world: `line_trace_single(actor.get_world(), ...)` (`LevelEditorSubsystem.get_world()` is a null context); `HitResult.component`/`actor` are read-protected, so read `export_text()`.
- World subsystems have no Python accessor — reach one via `ObjectIterator(unreal.YourSubsystem)` filtered on `get_outer().get_path_name()`.
- Never `save_dirty_packages(True, True)` — save by explicit path; a dirty package you did not touch is the user's work.
