Skip to content

Latest commit

 

History

History
398 lines (311 loc) · 16.3 KB

File metadata and controls

398 lines (311 loc) · 16.3 KB

Claude Can Build Your UEFN Game — Technical Breakdown

For skeptics. This document walks through exactly what happens when Claude builds a Fortnite Creative game autonomously using UEFN Toolbelt — every tool call, every output, and the hard limits of what's possible today.


What "Autonomous" Actually Means Here

Not "AI suggests code you copy-paste." Not "AI fills in a template you prompt manually."

The loop is:

  1. Claude reads your live level — transforms, classes, device labels, all 521 actors
  2. Claude reasons about the level — identifies game-logic devices, infers a game concept
  3. Claude writes valid Verse@editable refs wired to real device labels, full event logic
  4. Claude deploys it — writes the .verse file directly to the project's Verse directory
  5. You click Build Verse — it compiles

The only human action in steps 1–4 is pasting a single Python line into the UEFN console.


The Four Tools That Make It Possible

Tool What it does
world_state_export Snapshots every actor in the level to docs/world_state.json — label, class, transforms, readable properties
verse_gen_game_skeleton Generates a full creative_device Verse skeleton from a device name
verse_write_file Writes any Verse content directly to the project's Verse source directory (not the engine — the real project path)
verse_find_project_path Auto-detects the correct Verse folder by walking up from __file__, avoiding the unreal.Paths.project_dir() bug that returns the FortniteGame engine dir

Live Run — March 22, 2026

Level: Device_API_Mapping

  • 521 total actors in level
  • 40 Creative/Verse devices identified
  • 6187 bytes of Verse generated
  • Build result: VerseBuild: SUCCESS — zero errors, first compile

Step 1: Export world state

tb.run("world_state_export")

Output:

[TOOLBELT] world_state_export: capturing 521 actors...
[TOOLBELT] ✓ World state saved: Saved/UEFN_Toolbelt/world_state.json
[TOOLBELT] ✓ Auto-synced to repo: docs/world_state.json
{'status': 'ok', 'count': 521}

Step 2: Claude reads the JSON and identifies devices

From world_state.json, Claude identified:

Class Label Location
FortCreativeTimerDevice Timer Device (4, -784, 488)
FortCreativeTimerObjective Timed Objective (4, -784, 488)
FortCreativeRoundSettings Round Settings (-280, -796, 596)
BuildingProp_CaptureArea Capture Area (112, -100, 0)
BuildingProp_CaptureItemSpawner Capture Item Spawner (428, -108, 0)
BuildingProp_CaptureItemSpawner Capture Item Spawner2 (108, -668, 300)
BuildingProp_DeimosSpawner Creature Spawner (584, -1276, 36)
BuildingProp_CreatureManager Creature Manager (16, -696, 0)
BuildingProp_CreaturePlacer Creature Placer (176, -500, 0)
BuildingProp_SwitchDevice Button (7119, -1000, 115)
BuildingProp_SwitchDevice Button2 (72, -1000, 552)
FortCreativeLockDevice Conditional Button (9751, 1469, 101)
FortCreativeLockDevice Conditional Button2 (92, -996, 632)
BuildingProp_LockDevice Lock Device (36, -992, 576)
FortCreativeTeleporter × 14 Teleporter–Teleporter14 (various)
CreativeAudioMixerDevice Audio Mixer (608, -480, 0)
FortWeaponModStationBase Weapon Mod Bench (28, -1048, 344)
FortAthenaSplineZipline Zipline (-572, -268, 604)

Game concept inferred by Claude: Capture & Defend — players hold a capture area while creature waves attack; timer counts down the round; buttons control doors and conditional gates.

Step 3: Deploy

tb.run("verse_write_file",
       filename="device_api_game_manager.verse",
       content=verse_code,
       overwrite=True)

Output:

[TOOLBELT] ✓ Written: Device_API_Mapping\Verse\device_api_game_manager.verse (6187 bytes)
{'status': 'ok', 'bytes': 6187}

Step 4: Build result

VerseBuild: Compilation complete.
VerseBuild: Linking complete.
VerseBuild: SUCCESS -- Build complete.

The Generated File

# device_api_game_manager.verse
# AUTO-GENERATED by UEFN Toolbelt (world_state_export)
# Level: Device_API_Mapping   Actors: 521   Devices: 40+
# Generated: 2026-03-22

using { /Fortnite.com/Devices }
using { /Fortnite.com/Characters }
using { /Fortnite.com/Playspaces }
using { /Verse.org/Simulation }
using { /UnrealEngine.com/Temporary/Diagnostics }

device_api_game_manager := class(creative_device):

    @editable GameTimer        : timer_device           = timer_device{}
    @editable TimedObjective   : timed_objective_device = timed_objective_device{}
    @editable RoundSettings    : round_settings_device  = round_settings_device{}
    @editable CaptureArea      : capture_area_device    = capture_area_device{}
    @editable CaptureItemSpawner  : item_spawner_device = item_spawner_device{}
    @editable CaptureItemSpawner2 : item_spawner_device = item_spawner_device{}
    @editable CreatureSpawner  : guard_spawner_device   = guard_spawner_device{}
    @editable CreatureManager  : creature_manager_device = creature_manager_device{}
    @editable CreaturePlacer   : creature_placer_device = creature_placer_device{}
    @editable Button           : button_device          = button_device{}
    @editable Button2          : button_device          = button_device{}
    @editable ConditionalButton  : conditional_button_device = conditional_button_device{}
    @editable ConditionalButton2 : conditional_button_device = conditional_button_device{}
    @editable LockDevice       : lock_device            = lock_device{}
    @editable Teleporter11     : teleporter_device      = teleporter_device{}
    @editable Teleporter10     : teleporter_device      = teleporter_device{}
    @editable Teleporter12     : teleporter_device      = teleporter_device{}
    @editable Teleporter13     : teleporter_device      = teleporter_device{}
    @editable Teleporter14     : teleporter_device      = teleporter_device{}
    @editable AudioMixer       : audio_player_device    = audio_player_device{}
    @editable WeaponModBench   : weapon_mod_station_base = weapon_mod_station_base{}

    OnBegin<override>()<suspends> : void =
        GameTimer.Start()
        spawn { WatchCaptureArea() }
        spawn { WatchCreatureWaves() }
        Button.InteractedWithEvent.Subscribe(OnButtonPressed)
        Button2.InteractedWithEvent.Subscribe(OnButton2Pressed)
        GameTimer.SuccessEvent.Subscribe(OnTimerComplete)
        TimedObjective.ObjectiveCompleteEvent.Subscribe(OnObjectiveComplete)

    WatchCaptureArea()<suspends> : void =
        loop:
            CaptureEvent := CaptureArea.TeamChangedEvent.Await()
            CaptureItemSpawner.Spawn()
            CaptureItemSpawner2.Spawn()
            CreatureSpawner.Spawn()

    WatchCreatureWaves()<suspends> : void =
        loop:
            CreatureManager.Enable()
            Sleep(30.0)

    OnButtonPressed(Agent : agent) : void =
        LockDevice.Unlock(Agent)

    OnButton2Pressed(Agent : agent) : void =
        ConditionalButton.Activate(Agent)

    OnTimerComplete(Agent : agent) : void = EndRound()
    OnObjectiveComplete(Agent : agent) : void = EndRound()

    EndRound() : void =
        GameTimer.Stop()
        CreatureManager.Disable()
        CreatureSpawner.Despawn()
        AudioMixer.Stop()


Phase 2: The Device Catalog — Claude Knows Every Placeable Device

world_state_export answers: "What's in this level?"

device_catalog_scan answers: "What could be in any level?"

This is the difference between an AI that reacts to what you've built and an AI that can design from scratch. By scanning the full UEFN Asset Registry for every Blueprint class matching Creative device keywords across all Fortnite packages, Claude gets a complete palette of every device available — whether or not any of them are in the current level.

tb.run("device_catalog_scan")
# → Scanned 24,926 Blueprint assets across /Fortnite, /Game, /FortniteGame
# → 4,698 Creative devices identified across 35 categories
# → Saves to docs/device_catalog.json

Live run results — March 22, 2026 (first run ever):

Category Devices Category Devices
Prop 1,573 Spawner 94
Creative 504 Race 75
Stat 486 Audio 66
Lock 463 Beacon 47
Camera 446 Pad 43
NPC 173 Tracker 33
Round 144 Chest 32
Device 102 Zipline 31
Gate 95 Manager 29
Mutator 29 Score 28
Supply 25 Trigger 24
Guard 24 Zone 23
Button 22 Barrier 19
Ammo 16 Pulse 13
Creature 9 Timer 8
Capture 7 Teleporter 5
Switch 3 FortAthena 1

Total: 4,698 devices. 24,926 Blueprint assets scanned. 35 categories.

What this unlocks:

  • Claude can now say "this level needs a Score Manager — here's the exact asset path"
  • Claude can compare what's in the level vs what's available to place
  • Claude can generate Verse wiring for devices that don't exist in the level yet, then tell you exactly which Content Browser asset to drag in

The discovery that almost broke it: asset.object_path and asset.asset_class are deprecated in UEFN 40.00's Asset Registry API. The tool's original try/except: continue block was silently skipping all 24,926 assets because the deprecated property threw on every single call. The fix was to split the try/except: make asset_name the only required field, use get_full_name() and asset_class_path for the rest with individual fallbacks. Documented in UEFN_QUIRKS.md.

How it works under the hood:

ar = unreal.AssetRegistryHelpers.get_asset_registry()
flt = unreal.ARFilter(
    package_paths=["/Game"],          # /Fortnite and /FortniteGame are sandboxed (0 results)
    class_names=["Blueprint", "BlueprintGeneratedClass"],
    recursive_paths=True,
)
assets = ar.get_assets(flt)           # 24,926 Blueprint assets
# Filter by 35 device-hint keywords  → 4,698 matches
# Use get_full_name() not object_path → deprecated API fix
# → device_catalog.json

Results are grouped by category and saved to docs/device_catalog.json — git-tracked so Claude has the full palette available in every future session without re-scanning.


How It Works Under the Hood

Why world_state_export is the key primitive

Every other "AI-assisted UEFN" tool requires you to tell the AI what's in your level. world_state_export inverts this — Claude asks the level directly. The tool iterates every actor via EditorActorSubsystem, reads its transforms and all accessible properties, and writes a structured JSON. Claude can then reason over 521 actors in seconds.

Why verse_write_file required a non-obvious fix

unreal.Paths.project_dir() returns the FortniteGame engine directory (../../../FortniteGame/), not your project. Writing there would corrupt Epic's game files.

The fix is _find_uefn_project_root() — walk up from __file__ until hitting the Content folder, then use its parent. This is the actual UEFN project root regardless of where UEFN is installed. Documented in UEFN_QUIRKS.md.

Why V2 device properties aren't set at generation time

Fortnite V2 Creative devices (Timer, Capture Area, Score Manager, etc.) store their game-logic settings (duration, team_index, channel) as Verse @editable properties — not UPROPERTYs. set_editor_property raises an exception on these. getattr returns nothing.

The architecturally correct solution (and what Epic intends) is exactly what this generator does: create a Verse creative_device that holds @editable references to the target devices and configures them at on_begin. The generator IS the solution, not a workaround.

Full breakdown: UEFN_QUIRKS.md — Quirk #19.


Hard Limits — What Claude Cannot Do Yet

Limit Reason Workaround
Cannot trigger Verse compiler from Python Epic has not exposed a BuildVerseCode Python API Click Verse → Build Verse Code manually
Cannot set V2 device game-logic properties via Python Stored as Verse @editable, not UPROPERTYs Generate Verse code that sets them in OnBegin
Cannot call runtime-only device methods (e.g. timer_start(player)) from editor Requires a live player reference — editor session has none Call from Verse at game runtime
Cannot read Verse @editable property values set in the editor UI Not exposed to Python layer Read via world_state_export (base props only)

When Epic exposes a Python BuildVerseCode API, the loop becomes fully headless — no human clicks required at all.



Phase 5 — The Recursive Build + Fix Loop

This is the piece that makes the pipeline industrial. After any Verse build failure, Claude calls one tool and gets back everything it needs to fix the code and redeploy.

# After user clicks Build Verse and errors appear:
result = tb.run("verse_patch_errors")

# What Claude gets back:
{
  "build_status":  "FAILED",
  "error_count":   3,
  "errors": [
    {"file": "game_manager.verse", "line": 42, "col": 7,
     "message": "identifier 'capture_area_device' not found"},
    {"file": "game_manager.verse", "line": 87, "col": 4,
     "message": "no overload of 'Subscribe' takes 1 argument"},
  ],
  "files": {
    "game_manager.verse": "...full current content of the file..."
  },
  "next_step": "Fix 3 errors in the files listed, then call verse_write_file(overwrite=True) and rebuild."
}

# Claude reads errors + file, generates fix:
tb.run("verse_write_file", filename="game_manager.verse",
       content=fixed_content, overwrite=True)

# User clicks Build Verse again → repeat until:
# {"build_status": "SUCCESS", "error_count": 0}

How verse_patch_errors works internally

  1. Finds the most recent .log file in UEFN's Saved/Logs/ directory
  2. Scans every line for the Verse error pattern: path/file.verse(line:col): error message
  3. Also scans for VerseBuild SUCCESS/FAILED summary lines
  4. Deduplicates errors (same file+line+message appear multiple times in the log)
  5. For each erroring filename, walks the project Verse directory to find and read the file
  6. Returns errors + file contents + next_step instruction in one structured dict

Common errors Claude will encounter

Error message Root cause Fix
identifier 'X' not found Wrong Verse device type name Look up correct type in api_verse_get_schema
no overload of 'Subscribe' takes 1 argument Event handler wrong signature Add (Agent : agent) parameter
'X' is not a member of 'Y' Method doesn't exist on device Run api_crawl_selection to find real methods
expected expression Indentation or syntax error in generated code Fix whitespace / missing colon
type mismatch: expected X, got Y Wrong type passed to method Cast or use correct Verse type

The Roadmap to Full Autonomy

TODAY (fully operational):
  Phase 0: scaffold_generate, organize_assets
  Phase 1: world_state_export (521 actors), device_catalog_scan (4,698 devices)
  Phase 2: Claude reasoning (no tool)
  Phase 3: spawn_actor + set_actor_transform via MCP
  Phase 4: verse_gen_* + verse_write_file (6,187 bytes, VerseBuild SUCCESS proven)
  Phase 5: verse_patch_errors → Claude fixes → verse_write_file → [click Build] → repeat
  Phase 6: world_state_export + snapshot_save

  ONE HUMAN ACTION: clicking Build Verse (once per iteration)

NEXT (waiting for Epic):
  system_build_verse     ← expose BuildVerseCode to Python → Phase 5 becomes headless

FUTURE (zero human clicks):
  device_catalog_scan    → Claude designs the game from scratch
  world_state_export     → Claude reads the current level
  spawn_actor (MCP)      → Claude places all needed devices
  verse_write_file       → Claude deploys the code
  system_build_verse     → Claude triggers the compiler
  verse_patch_errors     → Claude reads errors + fixes + redeploys
  LOOP                   → until VerseBuild: SUCCESS
  snapshot_save          → checkpoint
  DONE                   → playable Fortnite Creative game, no human in the loop

The full pipeline reference: docs/PIPELINE.md


UEFN Toolbelt — Built by Ocean Bennett · 2026 · AGPL-3.0