Add UnrealDocGenerator tool and UE API skill

- ue_parser.py: position-based UE C++ header parser
- ue_markdown.py: compact agent-optimised Markdown renderer
- generate.py: two-pass CLI (parse-all → type index → render-all)
- samples/: representative UE headers (GeomUtils, AIController, GameplayTagsManager)
- .claude/skills/ue-api/: Claude Code skill for querying UE docs + source headers
- CLAUDE.md: architecture notes, usage, critical gotchas

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-24 06:55:05 -05:00
commit 93ca33c36a
12 changed files with 4387 additions and 0 deletions

View File

@@ -0,0 +1,5 @@
{
"name": "ue-api",
"description": "Unreal Engine API documentation skill — queries generated docs and source headers to answer questions about UE C++ APIs, class hierarchies, and system flows.",
"version": "1.0.0"
}

View File

@@ -0,0 +1,109 @@
---
name: ue-api
description: >
Use this skill when the user asks anything about Unreal Engine C++ APIs,
class hierarchies, system flows, or how engine subsystems work together.
Trigger phrases include: "how does X work in UE", "what is the flow for",
"which class handles", "what virtual functions", "UE API for", "Unreal Engine
architecture", and any question that names UE types like ACharacter,
UActorComponent, APlayerController, UBehaviorTreeComponent, APawn, etc.
Always use this skill for Unreal Engine questions — don't rely on training
data alone when local documentation is available.
---
# UE API Skill
Answer questions about Unreal Engine C++ APIs and system flows using the local
documentation corpus as the fast primary source, then source headers for depth.
## Configuration
Two environment variables control where to look:
| Variable | Purpose | Example |
|---|---|---|
| `UE_DOCS_PATH` | Root of the generated documentation | `/home/user/ue-docs` |
| `UE_ENGINE_ROOT` | UE engine source root (for header fallback) | `/home/user/UnrealEngine` |
**Resolve them at the start of every query:**
```bash
echo "$UE_DOCS_PATH"
echo "$UE_ENGINE_ROOT"
```
If `UE_DOCS_PATH` is unset, ask the user where their generated docs are before
proceeding. If `UE_ENGINE_ROOT` is unset, only ask when a question actually
requires source headers — don't interrupt doc-only queries.
The type index is always at `$UE_DOCS_PATH/type-index.txt`.
## Step 1 — Identify types, resolve paths
Extract UE type names from the question (e.g. `APlayerController`, `APawn`,
`UBehaviorTreeComponent`). Resolve all of them in a single grep:
```bash
grep "^APlayerController:\|^APawn:\|^ACharacter:" "$UE_DOCS_PATH/type-index.txt"
```
Paths in the index are relative to `$UE_DOCS_PATH` — prepend it when reading:
```bash
# index returns: AController: Engine/Classes/GameFramework/Controller.md
# read as:
Read "$UE_DOCS_PATH/Engine/Classes/GameFramework/Controller.md"
```
The `.md` files are compact by design — only items with C++ doc comments,
no deprecated entries, enums collapsed when undescribed.
## Step 2 — Follow the trail
Inline links in `*Inherits*:` lines and function signatures point to related
types. Follow them when the question spans multiple classes. A second grep on
`type-index.txt` is always cheaper than guessing paths.
## Step 3 — Escalate to source headers when docs aren't enough
The docs only surface items with C++ doc comments. Go to `.h` files when:
- The exact call order or implementation logic isn't described in any comment
- A function or member is absent from `.md` files (no doc comment)
- The question involves macros: `UCLASS`, `UPROPERTY`, `UFUNCTION`,
`DECLARE_DELEGATE_*`, `GENERATED_BODY`, etc.
- Private or protected members are relevant to the answer
- The user asks about edge-case behaviour ("what happens when X is null?")
Search under `$UE_ENGINE_ROOT/Engine/Source/` — e.g.:
```bash
Glob("**/*Controller*.h", path="$UE_ENGINE_ROOT/Engine/Source/Runtime/Engine")
Grep("void Possess", path="$UE_ENGINE_ROOT/Engine/Source")
```
## Output format
Lead with the direct answer or a concise flow description. For multi-step
flows use an ASCII sequence or numbered list. For single-class API questions,
a brief prose answer with the key function signatures is enough.
Cite every substantive claim — `(Controller.md)` for docs, `(Controller.h:142)`
for source. Mark source-derived facts as *implementation detail* since they can
change across engine versions; doc-derived facts reflect the stable API contract.
## Examples
**"How does APlayerController possess APawn?"**
→ check `$UE_DOCS_PATH` → grep type-index for AController, APawn,
APlayerController, ACharacter → read the four `.md` files → ASCII diagram
**"What virtual functions does ACharacter expose for movement?"**
→ grep for ACharacter, UCharacterMovementComponent → read docs → list virtuals
**"What does UFUNCTION(BlueprintCallable) expand to?"**
→ docs won't help (macro) → search `$UE_ENGINE_ROOT` for `UFUNCTION` definition
**"How does the behavior tree pick which task to run?"**
→ grep for UBehaviorTreeComponent, UBTCompositeNode, UBTDecorator → read docs
→ if execution order is still unclear, escalate to `BehaviorTreeComponent.h`

View File

@@ -0,0 +1,109 @@
---
name: ue-api
description: >
Use this skill when the user asks anything about Unreal Engine C++ APIs,
class hierarchies, system flows, or how engine subsystems work together.
Trigger phrases include: "how does X work in UE", "what is the flow for",
"which class handles", "what virtual functions", "UE API for", "Unreal Engine
architecture", and any question that names UE types like ACharacter,
UActorComponent, APlayerController, UBehaviorTreeComponent, APawn, etc.
Always use this skill for Unreal Engine questions — don't rely on training
data alone when local documentation is available.
---
# UE API Skill
Answer questions about Unreal Engine C++ APIs and system flows using the local
documentation corpus as the fast primary source, then source headers for depth.
## Configuration
Two environment variables control where to look:
| Variable | Purpose | Example |
|---|---|---|
| `UE_DOCS_PATH` | Root of the generated documentation | `/home/user/ue-docs` |
| `UE_ENGINE_ROOT` | UE engine source root (for header fallback) | `/home/user/UnrealEngine` |
**Resolve them at the start of every query:**
```bash
echo "$UE_DOCS_PATH"
echo "$UE_ENGINE_ROOT"
```
If `UE_DOCS_PATH` is unset, ask the user where their generated docs are before
proceeding. If `UE_ENGINE_ROOT` is unset, only ask when a question actually
requires source headers — don't interrupt doc-only queries.
The type index is always at `$UE_DOCS_PATH/type-index.txt`.
## Step 1 — Identify types, resolve paths
Extract UE type names from the question (e.g. `APlayerController`, `APawn`,
`UBehaviorTreeComponent`). Resolve all of them in a single grep:
```bash
grep "^APlayerController:\|^APawn:\|^ACharacter:" "$UE_DOCS_PATH/type-index.txt"
```
Paths in the index are relative to `$UE_DOCS_PATH` — prepend it when reading:
```bash
# index returns: AController: Engine/Classes/GameFramework/Controller.md
# read as:
Read "$UE_DOCS_PATH/Engine/Classes/GameFramework/Controller.md"
```
The `.md` files are compact by design — only items with C++ doc comments,
no deprecated entries, enums collapsed when undescribed.
## Step 2 — Follow the trail
Inline links in `*Inherits*:` lines and function signatures point to related
types. Follow them when the question spans multiple classes. A second grep on
`type-index.txt` is always cheaper than guessing paths.
## Step 3 — Escalate to source headers when docs aren't enough
The docs only surface items with C++ doc comments. Go to `.h` files when:
- The exact call order or implementation logic isn't described in any comment
- A function or member is absent from `.md` files (no doc comment)
- The question involves macros: `UCLASS`, `UPROPERTY`, `UFUNCTION`,
`DECLARE_DELEGATE_*`, `GENERATED_BODY`, etc.
- Private or protected members are relevant to the answer
- The user asks about edge-case behaviour ("what happens when X is null?")
Search under `$UE_ENGINE_ROOT/Engine/Source/` — e.g.:
```bash
Glob("**/*Controller*.h", path="$UE_ENGINE_ROOT/Engine/Source/Runtime/Engine")
Grep("void Possess", path="$UE_ENGINE_ROOT/Engine/Source")
```
## Output format
Lead with the direct answer or a concise flow description. For multi-step
flows use an ASCII sequence or numbered list. For single-class API questions,
a brief prose answer with the key function signatures is enough.
Cite every substantive claim — `(Controller.md)` for docs, `(Controller.h:142)`
for source. Mark source-derived facts as *implementation detail* since they can
change across engine versions; doc-derived facts reflect the stable API contract.
## Examples
**"How does APlayerController possess APawn?"**
→ check `$UE_DOCS_PATH` → grep type-index for AController, APawn,
APlayerController, ACharacter → read the four `.md` files → ASCII diagram
**"What virtual functions does ACharacter expose for movement?"**
→ grep for ACharacter, UCharacterMovementComponent → read docs → list virtuals
**"What does UFUNCTION(BlueprintCallable) expand to?"**
→ docs won't help (macro) → search `$UE_ENGINE_ROOT` for `UFUNCTION` definition
**"How does the behavior tree pick which task to run?"**
→ grep for UBehaviorTreeComponent, UBTCompositeNode, UBTDecorator → read docs
→ if execution order is still unclear, escalate to `BehaviorTreeComponent.h`

740
.gitignore vendored Normal file
View File

@@ -0,0 +1,740 @@
# ---> Python
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
.pybuilder/
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
# .python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# UV
# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
#uv.lock
# poetry
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
#poetry.lock
# pdm
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
#pdm.lock
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
# in version control.
# https://pdm.fming.dev/latest/usage/project/#working-with-version-control
.pdm.toml
.pdm-python
.pdm-build/
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# pytype static type analyzer
.pytype/
# Cython debug symbols
cython_debug/
# PyCharm
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/
# Ruff stuff:
.ruff_cache/
# PyPI configuration file
.pypirc
# ---> JetBrains
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
# User-specific stuff
.idea/**/workspace.xml
.idea/**/tasks.xml
.idea/**/usage.statistics.xml
.idea/**/dictionaries
.idea/**/shelf
# AWS User-specific
.idea/**/aws.xml
# Generated files
.idea/**/contentModel.xml
# Sensitive or high-churn files
.idea/**/dataSources/
.idea/**/dataSources.ids
.idea/**/dataSources.local.xml
.idea/**/sqlDataSources.xml
.idea/**/dynamic.xml
.idea/**/uiDesigner.xml
.idea/**/dbnavigator.xml
# Gradle
.idea/**/gradle.xml
.idea/**/libraries
# Gradle and Maven with auto-import
# When using Gradle or Maven with auto-import, you should exclude module files,
# since they will be recreated, and may cause churn. Uncomment if using
# auto-import.
# .idea/artifacts
# .idea/compiler.xml
# .idea/jarRepositories.xml
# .idea/modules.xml
# .idea/*.iml
# .idea/modules
# *.iml
# *.ipr
# CMake
cmake-build-*/
# Mongo Explorer plugin
.idea/**/mongoSettings.xml
# File-based project format
*.iws
# IntelliJ
out/
# mpeltonen/sbt-idea plugin
.idea_modules/
# JIRA plugin
atlassian-ide-plugin.xml
# Cursive Clojure plugin
.idea/replstate.xml
# SonarLint plugin
.idea/sonarlint/
# Crashlytics plugin (for Android Studio and IntelliJ)
com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
fabric.properties
# Editor-based Rest Client
.idea/httpRequests
# Android studio 3.1+ serialized cache file
.idea/caches/build_file_checksums.ser
# ---> VisualStudio
## Ignore Visual Studio temporary files, build results, and
## files generated by popular Visual Studio add-ons.
##
## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore
# User-specific files
*.rsuser
*.suo
*.user
*.userosscache
*.sln.docstates
# User-specific files (MonoDevelop/Xamarin Studio)
*.userprefs
# Mono auto generated files
mono_crash.*
# Build results
[Dd]ebug/
[Dd]ebugPublic/
[Rr]elease/
[Rr]eleases/
x64/
x86/
[Ww][Ii][Nn]32/
[Aa][Rr][Mm]/
[Aa][Rr][Mm]64/
bld/
[Bb]in/
[Oo]bj/
[Ll]og/
[Ll]ogs/
# Visual Studio 2015/2017 cache/options directory
.vs/
# Uncomment if you have tasks that create the project's static files in wwwroot
#wwwroot/
# Visual Studio 2017 auto generated files
Generated\ Files/
# MSTest test Results
[Tt]est[Rr]esult*/
[Bb]uild[Ll]og.*
# NUnit
*.VisualState.xml
TestResult.xml
nunit-*.xml
# Build Results of an ATL Project
[Dd]ebugPS/
[Rr]eleasePS/
dlldata.c
# Benchmark Results
BenchmarkDotNet.Artifacts/
# .NET Core
project.lock.json
project.fragment.lock.json
artifacts/
# ASP.NET Scaffolding
ScaffoldingReadMe.txt
# StyleCop
StyleCopReport.xml
# Files built by Visual Studio
*_i.c
*_p.c
*_h.h
*.ilk
*.meta
*.obj
*.iobj
*.pch
*.pdb
*.ipdb
*.pgc
*.pgd
*.rsp
# but not Directory.Build.rsp, as it configures directory-level build defaults
!Directory.Build.rsp
*.sbr
*.tlb
*.tli
*.tlh
*.tmp
*.tmp_proj
*_wpftmp.csproj
*.log
*.tlog
*.vspscc
*.vssscc
.builds
*.pidb
*.svclog
*.scc
# Chutzpah Test files
_Chutzpah*
# Visual C++ cache files
ipch/
*.aps
*.ncb
*.opendb
*.opensdf
*.sdf
*.cachefile
*.VC.db
*.VC.VC.opendb
# Visual Studio profiler
*.psess
*.vsp
*.vspx
*.sap
# Visual Studio Trace Files
*.e2e
# TFS 2012 Local Workspace
$tf/
# Guidance Automation Toolkit
*.gpState
# ReSharper is a .NET coding add-in
_ReSharper*/
*.[Rr]e[Ss]harper
*.DotSettings.user
# TeamCity is a build add-in
_TeamCity*
# DotCover is a Code Coverage Tool
*.dotCover
# AxoCover is a Code Coverage Tool
.axoCover/*
!.axoCover/settings.json
# Coverlet is a free, cross platform Code Coverage Tool
coverage*.json
coverage*.xml
coverage*.info
# Visual Studio code coverage results
*.coverage
*.coveragexml
# NCrunch
_NCrunch_*
.*crunch*.local.xml
nCrunchTemp_*
# MightyMoose
*.mm.*
AutoTest.Net/
# Web workbench (sass)
.sass-cache/
# Installshield output folder
[Ee]xpress/
# DocProject is a documentation generator add-in
DocProject/buildhelp/
DocProject/Help/*.HxT
DocProject/Help/*.HxC
DocProject/Help/*.hhc
DocProject/Help/*.hhk
DocProject/Help/*.hhp
DocProject/Help/Html2
DocProject/Help/html
# Click-Once directory
publish/
# Publish Web Output
*.[Pp]ublish.xml
*.azurePubxml
# Note: Comment the next line if you want to checkin your web deploy settings,
# but database connection strings (with potential passwords) will be unencrypted
*.pubxml
*.publishproj
# Microsoft Azure Web App publish settings. Comment the next line if you want to
# checkin your Azure Web App publish settings, but sensitive information contained
# in these scripts will be unencrypted
PublishScripts/
# NuGet Packages
*.nupkg
# NuGet Symbol Packages
*.snupkg
# The packages folder can be ignored because of Package Restore
**/[Pp]ackages/*
# except build/, which is used as an MSBuild target.
!**/[Pp]ackages/build/
# Uncomment if necessary however generally it will be regenerated when needed
#!**/[Pp]ackages/repositories.config
# NuGet v3's project.json files produces more ignorable files
*.nuget.props
*.nuget.targets
# Microsoft Azure Build Output
csx/
*.build.csdef
# Microsoft Azure Emulator
ecf/
rcf/
# Windows Store app package directories and files
AppPackages/
BundleArtifacts/
Package.StoreAssociation.xml
_pkginfo.txt
*.appx
*.appxbundle
*.appxupload
# Visual Studio cache files
# files ending in .cache can be ignored
*.[Cc]ache
# but keep track of directories ending in .cache
!?*.[Cc]ache/
# Others
ClientBin/
~$*
*~
*.dbmdl
*.dbproj.schemaview
*.jfm
*.pfx
*.publishsettings
orleans.codegen.cs
# Including strong name files can present a security risk
# (https://github.com/github/gitignore/pull/2483#issue-259490424)
#*.snk
# Since there are multiple workflows, uncomment next line to ignore bower_components
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
#bower_components/
# RIA/Silverlight projects
Generated_Code/
# Backup & report files from converting an old project file
# to a newer Visual Studio version. Backup files are not needed,
# because we have git ;-)
_UpgradeReport_Files/
Backup*/
UpgradeLog*.XML
UpgradeLog*.htm
ServiceFabricBackup/
*.rptproj.bak
# SQL Server files
*.mdf
*.ldf
*.ndf
# Business Intelligence projects
*.rdl.data
*.bim.layout
*.bim_*.settings
*.rptproj.rsuser
*- [Bb]ackup.rdl
*- [Bb]ackup ([0-9]).rdl
*- [Bb]ackup ([0-9][0-9]).rdl
# Microsoft Fakes
FakesAssemblies/
# GhostDoc plugin setting file
*.GhostDoc.xml
# Node.js Tools for Visual Studio
.ntvs_analysis.dat
node_modules/
# Visual Studio 6 build log
*.plg
# Visual Studio 6 workspace options file
*.opt
# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
*.vbw
# Visual Studio 6 auto-generated project file (contains which files were open etc.)
*.vbp
# Visual Studio 6 workspace and project file (working project files containing files to include in project)
*.dsw
*.dsp
# Visual Studio 6 technical files
*.ncb
*.aps
# Visual Studio LightSwitch build output
**/*.HTMLClient/GeneratedArtifacts
**/*.DesktopClient/GeneratedArtifacts
**/*.DesktopClient/ModelManifest.xml
**/*.Server/GeneratedArtifacts
**/*.Server/ModelManifest.xml
_Pvt_Extensions
# Paket dependency manager
.paket/paket.exe
paket-files/
# FAKE - F# Make
.fake/
# CodeRush personal settings
.cr/personal
# Python Tools for Visual Studio (PTVS)
__pycache__/
*.pyc
# Cake - Uncomment if you are using it
# tools/**
# !tools/packages.config
# Tabs Studio
*.tss
# Telerik's JustMock configuration file
*.jmconfig
# BizTalk build output
*.btp.cs
*.btm.cs
*.odx.cs
*.xsd.cs
# OpenCover UI analysis results
OpenCover/
# Azure Stream Analytics local run output
ASALocalRun/
# MSBuild Binary and Structured Log
*.binlog
# NVidia Nsight GPU debugger configuration file
*.nvuser
# MFractors (Xamarin productivity tool) working folder
.mfractor/
# Local History for Visual Studio
.localhistory/
# Visual Studio History (VSHistory) files
.vshistory/
# BeatPulse healthcheck temp database
healthchecksdb
# Backup folder for Package Reference Convert tool in Visual Studio 2017
MigrationBackup/
# Ionide (cross platform F# VS Code tools) working folder
.ionide/
# Fody - auto-generated XML schema
FodyWeavers.xsd
# VS Code files for those working on multiple tools
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
*.code-workspace
# Local History for Visual Studio Code
.history/
# Windows Installer files from build outputs
*.cab
*.msi
*.msix
*.msm
*.msp
# JetBrains Rider
*.sln.iml
# ---> VisualStudioCode
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
!.vscode/*.code-snippets
# Local History for Visual Studio Code
.history/
# Built Visual Studio Code Extensions
*.vsix
# ---> macOS
# General
.DS_Store
.AppleDouble
.LSOverride
# Icon must end with two \r
Icon
# Thumbnails
._*
# Files that might appear in the root of a volume
.DocumentRevisions-V100
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes
.VolumeIcon.icns
.com.apple.timemachine.donotpresent
# Directories potentially created on remote AFP share
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk
# ---> Linux
*~
# temporary files which can be created if a process still has a handle open of a deleted file
.fuse_hidden*
# KDE directory preferences
.directory
# Linux trash folder which might appear on any partition or disk
.Trash-*
# .nfs files are created when an open file is removed but is still being accessed
.nfs*
# ---> Windows
# Windows thumbnail cache files
Thumbs.db
Thumbs.db:encryptable
ehthumbs.db
ehthumbs_vista.db
# Dump file
*.stackdump
# Folder config file
[Dd]esktop.ini
# Recycle Bin used on file shares
$RECYCLE.BIN/
# Windows Installer files
*.cab
*.msi
*.msix
*.msm
*.msp
# Windows shortcuts
*.lnk

49
CLAUDE.md Normal file
View File

@@ -0,0 +1,49 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project Purpose
UnrealDocGenerator is a tool (in development) for generating documentation from Unreal Engine C++ header files. The `samples/` directory contains representative Unreal Engine headers used as test inputs:
- `samples/GeomUtils.h` — Small utility header (~6 KB): geometry/math helpers in `UE::AI` namespace
- `samples/AIController.h` — Medium header (~22 KB): `AAIController` class with movement, behavior tree, perception, and blackboard integration
- `samples/GameplayTagsManager.h` — Large/complex header (~43 KB): `UGameplayTagsManager` singleton with tag trees, INI/DataTable loading, and editor support
These samples cover a range of complexity (free functions, large classes, deeply nested types, macros, delegates) suitable for stress-testing a parser or doc generator.
## Current State
Implementation complete. Three Python files:
- `ue_parser.py` — Parses UE headers into dataclasses
- `ue_markdown.py` — Renders parsed data as Markdown (ultra-compact format, documented items only)
- `generate.py` — CLI entry point; two-pass pipeline (parse-all → build type index → render-all)
## Usage
```bash
python generate.py samples/ docs/ # process directory → docs/
python generate.py samples/AIController.h docs/ # single file
```
Output: one `.md` per `.h` + `docs/type-index.txt` (compact `TypeName: path/to/File.md` lookup).
Agent lookup pattern: `grep "^AController:" docs/type-index.txt` → instant single-line result.
## Architecture Notes
- Parser uses a **position-based scanner**, not line-by-line regex, to handle nested braces correctly.
- `find_matching_close()` extracts balanced `{}`/`()` while skipping `//`, `/* */`, and string literals.
- Only items with actual C++ doc comments (`/** */`) are included in output (`_has_doc()` filter).
- Two-pass pipeline in `generate.py`: Pass 1 parses all files, Pass 2 renders with cross-reference links.
- Inline cross-references: base class names linked to their `.md` when in corpus (`_make_type_link()`).
- No "See also" section — inline links in `*Inherits*:` lines cover cross-file navigation.
- Deprecated functions excluded from output (`not f.is_deprecated` in visibility filter).
- Enums with no value descriptions use compact inline format (`Values: A, B, C`) instead of a table.
- Placeholder descriptions (`------//`) filtered by `_PLACEHOLDER_RE` in `_fn_body()`.
## Critical Gotchas
- **Never** use `\s` inside a `[...]` character class with `*?` on C++ source — causes catastrophic regex backtracking (hangs on `AIController.h`). Use line-based scanning instead.
- Identifier regex must be `r'\w+'`, **not** `r'[\w:~]+'` — the latter consumes `public:` as one token, breaking access specifier detection (all members become `private`).
- Module name casing: use `_caps_to_camel()` (not `.title()`) — `AIMODULE.title()``Aimodule` instead of `AIModule`.

2
README.md Normal file
View File

@@ -0,0 +1,2 @@
# ue-ai-skill

135
generate.py Normal file
View File

@@ -0,0 +1,135 @@
#!/usr/bin/env python3
"""
generate.py — CLI for UnrealDocGenerator.
Usage:
python generate.py <input> <output_dir>
<input> can be a single .h file or a directory (processed recursively).
Two-pass pipeline:
Pass 1 — parse every header, build a corpus-wide type index
Pass 2 — render each header with cross-reference links injected
"""
import sys
import os
import re
from pathlib import Path
from ue_parser import parse_header, ParsedHeader
from ue_markdown import render_header
# ---------------------------------------------------------------------------
# Type index
# ---------------------------------------------------------------------------
def build_type_index(parsed_list: list[tuple[Path, ParsedHeader]],
input_base: Path) -> dict[str, str]:
"""
Returns {TypeName: md_path_relative_to_docs_root} for every
class, struct, enum, and delegate in the corpus.
"""
index: dict[str, str] = {}
for h, parsed in parsed_list:
try:
rel = h.relative_to(input_base)
except ValueError:
rel = Path(h.name)
md_rel = str(rel.with_suffix('.md'))
for ci in parsed.classes:
index[ci.name] = md_rel
for ei in parsed.enums:
index[ei.name] = md_rel
for di in parsed.delegates:
index[di.name] = md_rel
# namespace names are not types — skip
return index
# ---------------------------------------------------------------------------
# Type index file
# ---------------------------------------------------------------------------
def write_type_index(type_index: dict[str, str], output_dir: Path) -> None:
"""
Write docs/type-index.txt — compact TypeName: path/to/File.md lookup.
One entry per line, alphabetically sorted. Agents can grep this file
to resolve a type name to its documentation path.
"""
_valid_name = re.compile(r'^[A-Za-z_][A-Za-z0-9_]*$')
lines = sorted(f"{name}: {path}" for name, path in type_index.items()
if _valid_name.match(name))
out = output_dir / "type-index.txt"
out.write_text('\n'.join(lines) + '\n', encoding='utf-8')
print(f"Written {out}")
# ---------------------------------------------------------------------------
# Main
# ---------------------------------------------------------------------------
def main():
if len(sys.argv) < 3:
print("Usage: python generate.py <input> <output_dir>", file=sys.stderr)
sys.exit(1)
input_arg = Path(sys.argv[1])
output_dir = Path(sys.argv[2])
output_dir.mkdir(parents=True, exist_ok=True)
# Collect input files
if input_arg.is_file():
headers = [input_arg]
input_base = input_arg.parent
elif input_arg.is_dir():
headers = sorted(input_arg.rglob('*.h'))
input_base = input_arg
else:
print(f"Error: {input_arg} is not a file or directory", file=sys.stderr)
sys.exit(1)
if not headers:
print("No .h files found.", file=sys.stderr)
sys.exit(1)
# --- Pass 1: parse all ---
parsed_list: list[tuple[Path, ParsedHeader]] = []
for h in headers:
print(f"Parsing {h} ...")
try:
parsed = parse_header(str(h))
parsed_list.append((h, parsed))
except Exception as exc:
print(f" ERROR parsing {h}: {exc}", file=sys.stderr)
# --- Build corpus-wide type index ---
type_index = build_type_index(parsed_list, input_base)
# --- Pass 2: render all ---
success = 0
for h, parsed in parsed_list:
print(f"Rendering {h} ...")
try:
rel = h.relative_to(input_base)
except ValueError:
rel = Path(h.name)
current_md = str(rel.with_suffix('.md'))
out_path = output_dir / current_md
out_path.parent.mkdir(parents=True, exist_ok=True)
try:
md = render_header(parsed, type_index=type_index, current_md=current_md)
out_path.write_text(md, encoding='utf-8')
success += 1
except Exception as exc:
print(f" ERROR rendering {h}: {exc}", file=sys.stderr)
write_type_index(type_index, output_dir)
print(f"\nGenerated {success}/{len(parsed_list)} files + type-index.txt in {output_dir}/")
if __name__ == '__main__':
main()

465
samples/AIController.h Normal file
View File

@@ -0,0 +1,465 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "UObject/ObjectMacros.h"
#include "UObject/UObjectGlobals.h"
#include "Templates/SubclassOf.h"
#include "EngineDefines.h"
#if UE_ENABLE_INCLUDE_ORDER_DEPRECATED_IN_5_4
#include "NavFilters/NavigationQueryFilter.h"
#endif
#include "AITypes.h"
#include "GameplayTaskOwnerInterface.h"
#include "GameplayTask.h"
#include "GameFramework/Pawn.h"
#include "GameFramework/Controller.h"
#include "Perception/AIPerceptionListenerInterface.h"
#include "GenericTeamAgentInterface.h"
#include "VisualLogger/VisualLoggerDebugSnapshotInterface.h"
#include "AIController.generated.h"
class FDebugDisplayInfo;
class UAIPerceptionComponent;
class UBehaviorTree;
class UBlackboardComponent;
class UBlackboardData;
class UBrainComponent;
class UCanvas;
class UGameplayTaskResource;
class UGameplayTasksComponent;
class UPathFollowingComponent;
namespace EPathFollowingRequestResult { enum Type : int; }
namespace EPathFollowingResult { enum Type : int; }
namespace EPathFollowingStatus { enum Type : int; }
#if ENABLE_VISUAL_LOG
struct FVisualLogEntry;
#endif // ENABLE_VISUAL_LOG
struct FPathFindingQuery;
struct FPathFollowingRequestResult;
struct FPathFollowingResult;
DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FAIMoveCompletedSignature, FAIRequestID, RequestID, EPathFollowingResult::Type, Result);
// the reason for this being namespace instead of a regular enum is
// so that it can be expanded in game-specific code
// @todo this is a bit messy, needs to be refactored
namespace EAIFocusPriority
{
typedef uint8 Type;
inline const Type Default = 0;
inline const Type Move = 1;
inline const Type Gameplay = 2;
inline const Type LastFocusPriority = Gameplay;
}
struct FFocusKnowledge
{
struct FFocusItem
{
TWeakObjectPtr<AActor> Actor;
FVector Position;
FFocusItem()
{
Actor = nullptr;
Position = FAISystem::InvalidLocation;
}
};
TArray<FFocusItem> Priorities;
};
//~=============================================================================
/**
* AIController is the base class of controllers for AI-controlled Pawns.
*
* Controllers are non-physical actors that can be attached to a pawn to control its actions.
* AIControllers manage the artificial intelligence for the pawns they control.
* In networked games, they only exist on the server.
*
* @see https://docs.unrealengine.com/latest/INT/Gameplay/Framework/Controller/
*/
UCLASS(ClassGroup = AI, BlueprintType, Blueprintable, MinimalAPI)
class AAIController : public AController, public IAIPerceptionListenerInterface, public IGameplayTaskOwnerInterface, public IGenericTeamAgentInterface, public IVisualLoggerDebugSnapshotInterface
{
GENERATED_BODY()
FGameplayResourceSet ScriptClaimedResources;
protected:
FFocusKnowledge FocusInformation;
/** By default AI's logic does not start when controlled Pawn is possessed. Setting this flag to true
* will make AI logic start when pawn is possessed */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = AI)
uint32 bStartAILogicOnPossess : 1;
/** By default AI's logic gets stopped when controlled Pawn is unpossessed. Setting this flag to false
* will make AI logic persist past losing control over a pawn */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = AI)
uint32 bStopAILogicOnUnposses : 1;
public:
/** used for alternating LineOfSight traces */
UPROPERTY()
mutable uint32 bLOSflag : 1;
/** Skip extra line of sight traces to extremities of target being checked. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = AI)
uint32 bSkipExtraLOSChecks : 1;
/** Is strafing allowed during movement? */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = AI)
uint32 bAllowStrafe : 1;
/** Specifies if this AI wants its own PlayerState. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = AI)
uint32 bWantsPlayerState : 1;
/** Copy Pawn rotation to ControlRotation, if there is no focus point. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = AI)
uint32 bSetControlRotationFromPawnOrientation:1;
private:
/** Component used for moving along a path. */
UPROPERTY(VisibleDefaultsOnly, Category = AI)
TObjectPtr<UPathFollowingComponent> PathFollowingComponent;
public:
/** Component responsible for behaviors. */
UPROPERTY(BlueprintReadWrite, Category = AI)
TObjectPtr<UBrainComponent> BrainComponent;
UPROPERTY(VisibleDefaultsOnly, Category = AI)
TObjectPtr<UAIPerceptionComponent> PerceptionComponent;
protected:
/** blackboard */
UPROPERTY(BlueprintReadOnly, Category = AI, meta = (AllowPrivateAccess = "true"))
TObjectPtr<UBlackboardComponent> Blackboard;
UPROPERTY()
TObjectPtr<UGameplayTasksComponent> CachedGameplayTasksComponent;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = AI)
TSubclassOf<UNavigationQueryFilter> DefaultNavigationFilterClass;
public:
AIMODULE_API AAIController(const FObjectInitializer& ObjectInitializer = FObjectInitializer::Get());
AIMODULE_API virtual void SetPawn(APawn* InPawn) override;
/** Makes AI go toward specified Goal actor (destination will be continuously updated), aborts any active path following
* @param AcceptanceRadius - finish move if pawn gets close enough
* @param bStopOnOverlap - add pawn's radius to AcceptanceRadius
* @param bUsePathfinding - use navigation data to calculate path (otherwise it will go in straight line)
* @param bCanStrafe - set focus related flag: bAllowStrafe
* @param FilterClass - navigation filter for pathfinding adjustments. If none specified DefaultNavigationFilterClass will be used
* @param bAllowPartialPath - use incomplete path when goal can't be reached
* @note AcceptanceRadius has default value or -1 due to Header Parser not being able to recognize UPathFollowingComponent::DefaultAcceptanceRadius
*/
UFUNCTION(BlueprintCallable, Category = "AI|Navigation", Meta = (AdvancedDisplay = "bStopOnOverlap,bCanStrafe,bAllowPartialPath"))
AIMODULE_API EPathFollowingRequestResult::Type MoveToActor(AActor* Goal, float AcceptanceRadius = -1, bool bStopOnOverlap = true,
bool bUsePathfinding = true, bool bCanStrafe = true,
TSubclassOf<UNavigationQueryFilter> FilterClass = {}, bool bAllowPartialPath = true);
/** Makes AI go toward specified Dest location, aborts any active path following
* @param AcceptanceRadius - finish move if pawn gets close enough
* @param bStopOnOverlap - add pawn's radius to AcceptanceRadius
* @param bUsePathfinding - use navigation data to calculate path (otherwise it will go in straight line)
* @param bProjectDestinationToNavigation - project location on navigation data before using it
* @param bCanStrafe - set focus related flag: bAllowStrafe
* @param FilterClass - navigation filter for pathfinding adjustments. If none specified DefaultNavigationFilterClass will be used
* @param bAllowPartialPath - use incomplete path when goal can't be reached
* @note AcceptanceRadius has default value or -1 due to Header Parser not being able to recognize UPathFollowingComponent::DefaultAcceptanceRadius
*/
UFUNCTION(BlueprintCallable, Category = "AI|Navigation", Meta = (AdvancedDisplay = "bStopOnOverlap,bCanStrafe,bAllowPartialPath"))
AIMODULE_API EPathFollowingRequestResult::Type MoveToLocation(const FVector& Dest, float AcceptanceRadius = -1, bool bStopOnOverlap = true,
bool bUsePathfinding = true, bool bProjectDestinationToNavigation = false, bool bCanStrafe = true,
TSubclassOf<UNavigationQueryFilter> FilterClass = {}, bool bAllowPartialPath = true);
/** Makes AI go toward specified destination
* @param MoveRequest - details about move
* @param OutPath - optional output param, filled in with assigned path
* @return struct holding MoveId and enum code
*/
AIMODULE_API virtual FPathFollowingRequestResult MoveTo(const FAIMoveRequest& MoveRequest, FNavPathSharedPtr* OutPath = nullptr);
/** Passes move request and path object to path following */
AIMODULE_API virtual FAIRequestID RequestMove(const FAIMoveRequest& MoveRequest, FNavPathSharedPtr Path);
/** Finds path for given move request
* @param MoveRequest - details about move
* @param Query - pathfinding query for navigation system
* @param OutPath - generated path
*/
AIMODULE_API virtual void FindPathForMoveRequest(const FAIMoveRequest& MoveRequest, FPathFindingQuery& Query, FNavPathSharedPtr& OutPath) const;
/** Helper function for creating pathfinding query for this agent from move request data */
AIMODULE_API bool BuildPathfindingQuery(const FAIMoveRequest& MoveRequest, FPathFindingQuery& OutQuery) const;
/** Helper function for creating pathfinding query for this agent from move request data and starting location */
AIMODULE_API bool BuildPathfindingQuery(const FAIMoveRequest& MoveRequest, const FVector& StartLocation, FPathFindingQuery& OutQuery) const;
UE_DEPRECATED_FORGAME(4.13, "This function is now deprecated, please use FindPathForMoveRequest() for adjusting Query or BuildPathfindingQuery() for getting one.")
AIMODULE_API virtual bool PreparePathfinding(const FAIMoveRequest& MoveRequest, FPathFindingQuery& Query);
UE_DEPRECATED_FORGAME(4.13, "This function is now deprecated, please use FindPathForMoveRequest() for adjusting pathfinding or path postprocess.")
AIMODULE_API virtual FAIRequestID RequestPathAndMove(const FAIMoveRequest& MoveRequest, FPathFindingQuery& Query);
/** if AI is currently moving due to request given by RequestToPause, then the move will be paused */
AIMODULE_API bool PauseMove(FAIRequestID RequestToPause);
/** resumes last AI-performed, paused request provided it's ID was equivalent to RequestToResume */
AIMODULE_API bool ResumeMove(FAIRequestID RequestToResume);
/** Aborts the move the controller is currently performing */
AIMODULE_API virtual void StopMovement() override;
/** Called on completing current movement request */
AIMODULE_API virtual void OnMoveCompleted(FAIRequestID RequestID, const FPathFollowingResult& Result);
UE_DEPRECATED_FORGAME(4.13, "This function is now deprecated, please use version with EPathFollowingResultDetails parameter.")
AIMODULE_API virtual void OnMoveCompleted(FAIRequestID RequestID, EPathFollowingResult::Type Result);
/** Returns the Move Request ID for the current move */
AIMODULE_API FAIRequestID GetCurrentMoveRequestID() const;
/** Blueprint notification that we've completed the current movement request */
UPROPERTY(BlueprintAssignable, meta = (DisplayName = "MoveCompleted"))
FAIMoveCompletedSignature ReceiveMoveCompleted;
TSubclassOf<UNavigationQueryFilter> GetDefaultNavigationFilterClass() const { return DefaultNavigationFilterClass; }
/** Returns status of path following */
UFUNCTION(BlueprintCallable, Category = "AI|Navigation")
AIMODULE_API EPathFollowingStatus::Type GetMoveStatus() const;
/** Returns true if the current PathFollowingComponent's path is partial (does not reach desired destination). */
UFUNCTION(BlueprintCallable, Category = "AI|Navigation")
AIMODULE_API bool HasPartialPath() const;
/** Returns position of current path segment's end. */
UFUNCTION(BlueprintCallable, Category = "AI|Navigation")
AIMODULE_API FVector GetImmediateMoveDestination() const;
/** Updates state of movement block detection. */
UFUNCTION(BlueprintCallable, Category = "AI|Navigation")
AIMODULE_API void SetMoveBlockDetection(bool bEnable);
/** Starts executing behavior tree. */
UFUNCTION(BlueprintCallable, Category = "AI")
AIMODULE_API virtual bool RunBehaviorTree(UBehaviorTree* BTAsset);
protected:
AIMODULE_API virtual void CleanupBrainComponent();
/** Merges the remaining points of InitialPath, with the points of InOutMergedPath. The resulting merged path is outputted into InOutMergedPath. */
AIMODULE_API virtual void MergePaths(const FNavPathSharedPtr& InitialPath, FNavPathSharedPtr& InOutMergedPath) const;
public:
/**
* Makes AI use the specified Blackboard asset & creates a Blackboard Component if one does not already exist.
* @param BlackboardAsset The Blackboard asset to use.
* @param BlackboardComponent The Blackboard component that was used or created to work with the passed-in Blackboard Asset.
* @return true if we successfully linked the blackboard asset to the blackboard component.
*/
UFUNCTION(BlueprintCallable, Category = "AI")
AIMODULE_API bool UseBlackboard(UBlackboardData* BlackboardAsset, UBlackboardComponent*& BlackboardComponent);
/** does this AIController allow given UBlackboardComponent sync data with it */
AIMODULE_API virtual bool ShouldSyncBlackboardWith(const UBlackboardComponent& OtherBlackboardComponent) const;
UFUNCTION(BlueprintCallable, Category = "AI|Tasks")
AIMODULE_API void ClaimTaskResource(TSubclassOf<UGameplayTaskResource> ResourceClass);
UFUNCTION(BlueprintCallable, Category = "AI|Tasks")
AIMODULE_API void UnclaimTaskResource(TSubclassOf<UGameplayTaskResource> ResourceClass);
protected:
UFUNCTION(BlueprintImplementableEvent)
AIMODULE_API void OnUsingBlackBoard(UBlackboardComponent* BlackboardComp, UBlackboardData* BlackboardAsset);
AIMODULE_API virtual bool InitializeBlackboard(UBlackboardComponent& BlackboardComp, UBlackboardData& BlackboardAsset);
public:
/** Retrieve the final position that controller should be looking at. */
UFUNCTION(BlueprintCallable, Category = "AI")
AIMODULE_API FVector GetFocalPoint() const;
AIMODULE_API FVector GetFocalPointForPriority(EAIFocusPriority::Type InPriority) const;
/** Retrieve the focal point this controller should focus to on given actor. */
UFUNCTION(BlueprintCallable, Category = "AI")
AIMODULE_API virtual FVector GetFocalPointOnActor(const AActor *Actor) const;
/** Set the position that controller should be looking at. */
UFUNCTION(BlueprintCallable, Category = "AI", meta = (DisplayName = "SetFocalPoint", ScriptName = "SetFocalPoint", Keywords = "focus"))
AIMODULE_API void K2_SetFocalPoint(FVector FP);
/** Set Focus for actor, will set FocalPoint as a result. */
UFUNCTION(BlueprintCallable, Category = "AI", meta = (DisplayName = "SetFocus", ScriptName = "SetFocus"))
AIMODULE_API void K2_SetFocus(AActor* NewFocus);
/** Get the focused actor. */
UFUNCTION(BlueprintCallable, Category = "AI")
AIMODULE_API AActor* GetFocusActor() const;
inline AActor* GetFocusActorForPriority(EAIFocusPriority::Type InPriority) const { return FocusInformation.Priorities.IsValidIndex(InPriority) ? FocusInformation.Priorities[InPriority].Actor.Get() : nullptr; }
/** Clears Focus, will also clear FocalPoint as a result */
UFUNCTION(BlueprintCallable, Category = "AI", meta = (DisplayName = "ClearFocus", ScriptName = "ClearFocus"))
AIMODULE_API void K2_ClearFocus();
/**
* Computes a launch velocity vector to toss a projectile and hit the given destination.
* Performance note: Potentially expensive. Nonzero CollisionRadius and bOnlyTraceUp=false are the more expensive options.
*
* @param OutTossVelocity - out param stuffed with the computed velocity to use
* @param Start - desired start point of arc
* @param End - desired end point of arc
* @param TossSpeed - Initial speed of the theoretical projectile. Assumed to only change due to gravity for the entire lifetime of the projectile
* @param CollisionSize (optional) - is the size of bounding box of the tossed actor (defaults to (0,0,0)
* @param bOnlyTraceUp (optional) - when true collision checks verifying the arc will only be done along the upward portion of the arc
* @return - true if a valid arc was computed, false if no valid solution could be found
*/
AIMODULE_API bool SuggestTossVelocity(FVector& OutTossVelocity, FVector Start, FVector End, float TossSpeed, bool bPreferHighArc, float CollisionRadius = 0, bool bOnlyTraceUp = false);
//~ Begin AActor Interface
AIMODULE_API virtual void Tick(float DeltaTime) override;
AIMODULE_API virtual void PostInitializeComponents() override;
AIMODULE_API virtual void PostRegisterAllComponents() override;
//~ End AActor Interface
//~ Begin AController Interface
protected:
AIMODULE_API virtual void OnPossess(APawn* InPawn) override;
AIMODULE_API virtual void OnUnPossess() override;
public:
AIMODULE_API virtual bool ShouldPostponePathUpdates() const override;
AIMODULE_API virtual void DisplayDebug(UCanvas* Canvas, const FDebugDisplayInfo& DebugDisplay, float& YL, float& YPos) override;
#if ENABLE_VISUAL_LOG
AIMODULE_API virtual void GrabDebugSnapshot(FVisualLogEntry* Snapshot) const override;
#endif
AIMODULE_API virtual void Reset() override;
/**
* Checks line to center and top of other actor
* @param Other is the actor whose visibility is being checked.
* @param ViewPoint is eye position visibility is being checked from. If vect(0,0,0) passed in, uses current viewtarget's eye position.
* @param bAlternateChecks used only in AIController implementation
* @return true if controller's pawn can see Other actor.
*/
AIMODULE_API virtual bool LineOfSightTo(const AActor* Other, FVector ViewPoint = FVector(ForceInit), bool bAlternateChecks = false) const override;
//~ End AController Interface
/** Notifies AIController of changes in given actors' perception */
AIMODULE_API virtual void ActorsPerceptionUpdated(const TArray<AActor*>& UpdatedActors);
/** Update direction AI is looking based on FocalPoint */
AIMODULE_API virtual void UpdateControlRotation(float DeltaTime, bool bUpdatePawn = true);
/** Set FocalPoint for given priority as absolute position or offset from base. */
AIMODULE_API virtual void SetFocalPoint(FVector NewFocus, EAIFocusPriority::Type InPriority = EAIFocusPriority::Gameplay);
/* Set Focus actor for given priority, will set FocalPoint as a result. */
AIMODULE_API virtual void SetFocus(AActor* NewFocus, EAIFocusPriority::Type InPriority = EAIFocusPriority::Gameplay);
/** Clears Focus for given priority, will also clear FocalPoint as a result
* @param InPriority focus priority to clear. If you don't know what to use you probably mean EAIFocusPriority::Gameplay*/
AIMODULE_API virtual void ClearFocus(EAIFocusPriority::Type InPriority);
AIMODULE_API void SetPerceptionComponent(UAIPerceptionComponent& InPerceptionComponent);
//----------------------------------------------------------------------//
// IAIPerceptionListenerInterface
//----------------------------------------------------------------------//
virtual UAIPerceptionComponent* GetPerceptionComponent() override { return GetAIPerceptionComponent(); }
//----------------------------------------------------------------------//
// INavAgentInterface
//----------------------------------------------------------------------//
AIMODULE_API virtual bool IsFollowingAPath() const override;
AIMODULE_API virtual IPathFollowingAgentInterface* GetPathFollowingAgent() const override;
//----------------------------------------------------------------------//
// IGenericTeamAgentInterface
//----------------------------------------------------------------------//
private:
FGenericTeamId TeamID;
public:
AIMODULE_API virtual void SetGenericTeamId(const FGenericTeamId& NewTeamID) override;
virtual FGenericTeamId GetGenericTeamId() const override { return TeamID; }
//----------------------------------------------------------------------//
// IGameplayTaskOwnerInterface
//----------------------------------------------------------------------//
virtual UGameplayTasksComponent* GetGameplayTasksComponent(const UGameplayTask& Task) const override { return GetGameplayTasksComponent(); }
virtual AActor* GetGameplayTaskOwner(const UGameplayTask* Task) const override { return const_cast<AAIController*>(this); }
virtual AActor* GetGameplayTaskAvatar(const UGameplayTask* Task) const override { return GetPawn(); }
virtual uint8 GetGameplayTaskDefaultPriority() const { return FGameplayTasks::DefaultPriority - 1; }
inline UGameplayTasksComponent* GetGameplayTasksComponent() const { return CachedGameplayTasksComponent; }
// add empty overrides to fix linker errors if project implements a child class without adding GameplayTasks module dependency
virtual void OnGameplayTaskInitialized(UGameplayTask& Task) override {}
virtual void OnGameplayTaskActivated(UGameplayTask& Task) override {}
virtual void OnGameplayTaskDeactivated(UGameplayTask& Task) override {}
UFUNCTION()
AIMODULE_API virtual void OnGameplayTaskResourcesClaimed(FGameplayResourceSet NewlyClaimed, FGameplayResourceSet FreshlyReleased);
//----------------------------------------------------------------------//
// debug/dev-time
//----------------------------------------------------------------------//
AIMODULE_API virtual FString GetDebugIcon() const;
// Cheat/debugging functions
static void ToggleAIIgnorePlayers() { bAIIgnorePlayers = !bAIIgnorePlayers; }
static bool AreAIIgnoringPlayers() { return bAIIgnorePlayers; }
/** If true, AI controllers will ignore players. */
static AIMODULE_API bool bAIIgnorePlayers;
public:
/** Returns PathFollowingComponent subobject **/
UFUNCTION(BlueprintCallable, Category="AI|Navigation")
UPathFollowingComponent* GetPathFollowingComponent() const { return PathFollowingComponent; }
UFUNCTION(BlueprintPure, Category = "AI|Perception")
UAIPerceptionComponent* GetAIPerceptionComponent() { return PerceptionComponent; }
const UAIPerceptionComponent* GetAIPerceptionComponent() const { return PerceptionComponent; }
UBrainComponent* GetBrainComponent() const { return BrainComponent; }
const UBlackboardComponent* GetBlackboardComponent() const { return Blackboard; }
UBlackboardComponent* GetBlackboardComponent() { return Blackboard; }
/** Note that this function does not do any pathfollowing state transfer.
* Intended to be called as part of initialization/setup process */
UFUNCTION(BlueprintCallable, Category = "AI|Navigation")
AIMODULE_API void SetPathFollowingComponent(UPathFollowingComponent* NewPFComponent);
};
//----------------------------------------------------------------------//
// forceinlines
//----------------------------------------------------------------------//
namespace FAISystem
{
inline bool IsValidControllerAndHasValidPawn(const AController* Controller)
{
return Controller != nullptr && Controller->IsPendingKillPending() == false
&& Controller->GetPawn() != nullptr && Controller->GetPawn()->IsPendingKillPending() == false;
}
}

File diff suppressed because it is too large Load Diff

175
samples/GeomUtils.h Normal file
View File

@@ -0,0 +1,175 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#pragma once
#include "Containers/ContainersFwd.h"
#include "Math/UnrealMathSSE.h"
#include "Math/Vector.h"
#include "Math/Vector2D.h"
namespace UE::AI
{
/** @return 2D cross product. Using X and Y components of 3D vectors. */
inline FVector::FReal Cross2D(const FVector& A, const FVector& B)
{
return A.X * B.Y - A.Y * B.X;
}
/** @return 2D cross product. */
inline FVector2D::FReal Cross2D(const FVector2D& A, const FVector2D& B)
{
return A.X * B.Y - A.Y * B.X;
}
/** @return 2D area of triangle. Using X and Y components of 3D vectors. */
inline FVector::FReal TriArea2D(const FVector& A, const FVector& B, const FVector& C)
{
const FVector AB = B - A;
const FVector AC = C - A;
return (AC.X * AB.Y - AB.X * AC.Y) * 0.5;
}
/** @return 2D area of triangle. */
inline FVector2D::FReal TriArea2D(const FVector2D& A, const FVector2D& B, const FVector2D& C)
{
const FVector2D AB = B - A;
const FVector2D AC = C - A;
return (AC.X * AB.Y - AB.X * AC.Y) * 0.5;
}
/** @return value in range [0..1] of the 'Point' project on segment 'Start-End'. Using X and Y components of 3D vectors. */
inline FVector2D::FReal ProjectPointOnSegment2D(const FVector Point, const FVector Start, const FVector End)
{
using FReal = FVector::FReal;
const FVector2D Seg(End - Start);
const FVector2D Dir(Point - Start);
const FReal D = Seg.SquaredLength();
const FReal T = FVector2D::DotProduct(Seg, Dir);
if (T < 0.0)
{
return 0.0;
}
else if (T > D)
{
return 1.0;
}
return D > UE_KINDA_SMALL_NUMBER ? (T / D) : 0.0;
}
/** @return value of the 'Point' project on infinite line defined by segment 'Start-End'. Using X and Y components of 3D vectors. */
inline FVector::FReal ProjectPointOnLine2D(const FVector Point, const FVector Start, const FVector End)
{
using FReal = FVector::FReal;
const FVector2D Seg(End - Start);
const FVector2D Dir(Point - Start);
const FReal D = Seg.SquaredLength();
const FReal T = FVector2D::DotProduct(Seg, Dir);
return D > UE_KINDA_SMALL_NUMBER ? (T / D) : 0.0;
}
/** @return signed distance of the 'Point' to infinite line defined by segment 'Start-End'. Using X and Y components of 3D vectors. */
inline FVector::FReal SignedDistancePointLine2D(const FVector Point, const FVector Start, const FVector End)
{
using FReal = FVector::FReal;
const FVector2D Seg(End - Start);
const FVector2D Dir(Point - Start);
const FReal Nom = Cross2D(Seg, Dir);
const FReal Den = Seg.SquaredLength();
const FReal Dist = Den > UE_KINDA_SMALL_NUMBER ? (Nom / FMath::Sqrt(Den)) : 0.0;
return Dist;
}
/**
* Intersects infinite lines defined by segments A and B in 2D. Using X and Y components of 3D vectors.
* @param StartA start point of segment A
* @param EndA end point of segment A
* @param StartB start point of segment B
* @param EndB end point of segment B
* @param OutTA intersection value along segment A
* @param OutTB intersection value along segment B
* @return if segments A and B intersect in 2D
*/
inline bool IntersectLineLine2D(const FVector& StartA, const FVector& EndA, const FVector& StartB, const FVector& EndB, FVector2D::FReal& OutTA, FVector2D::FReal& OutTB)
{
using FReal = FVector::FReal;
const FVector U = EndA - StartA;
const FVector V = EndB - StartB;
const FVector W = StartA - StartB;
const FReal D = Cross2D(U, V);
if (FMath::Abs(D) < UE_KINDA_SMALL_NUMBER)
{
OutTA = 0.0;
OutTB = 0.0;
return false;
}
OutTA = Cross2D(V, W) / D;
OutTB = Cross2D(U, W) / D;
return true;
}
/**
* Calculates intersection of segment Start-End with convex polygon Poly in 2D. Using X and Y components of 3D vectors.
* @param Start start point of the segment
* @param End end point of the segment
* @param Poly convex polygon
* @param OutTMin value along the segment of the first intersection point [0..1]
* @param OutTMax value along the segment of the second intersection point [0..1]
* @param OutSegMin index of the polygon segment of the first intersection point
* @param OutSegMax index of the polygon segment of the second intersection point
* @return true if the segment inside or intersects with the polygon.
*/
extern AIMODULE_API bool IntersectSegmentPoly2D(const FVector& Start, const FVector& End, TConstArrayView<FVector> Poly,
FVector2D::FReal& OutTMin, FVector2D::FReal& OutTMax, int32& OutSegMin, int32& OutSegMax);
/**
* Interpolates bilinear patch A,B,C,D. U interpolates from A->B, and C->D, and V interpolates from AB->CD.
* @param UV interpolation coordinates [0..1] range
* @param VertexA first corner
* @param VertexB second corner
* @param VertexC third corner
* @param VertexD fourth corner
* @return interpolated value.
*/
inline FVector Bilinear(const FVector2D UV, const FVector VertexA, const FVector VertexB, const FVector VertexC, const FVector VertexD)
{
const FVector AB = FMath::Lerp(VertexA, VertexB, UV.X);
const FVector CD = FMath::Lerp(VertexD, VertexC, UV.X);
return FMath::Lerp(AB, CD, UV.Y);
}
/**
* Finds the UV coordinates of the 'Point' on bilinear patch A,B,C,D. U interpolates from A->B, and C->D, and V interpolates from AB->CD.
* @param Point location inside or close to the bilinear patch
* @param VertexA first corner
* @param VertexB second corner
* @param VertexC third corner
* @param VertexD fourth corner
* @return UV interpolation coordinates of the 'Point'.
*/
extern AIMODULE_API FVector2D InvBilinear2D(const FVector Point, const FVector VertexA, const FVector VertexB, const FVector VertexC, const FVector VertexD);
/**
* Finds the UV coordinates of the 'Point' on bilinear patch A,B,C,D. U interpolates from A->B, and C->D, and V interpolates from AB->CD.
* The UV coordinate is clamped to [0..1] range after inversion.
* @param Point location inside or close to the bilinear patch
* @param VertexA first corner
* @param VertexB second corner
* @param VertexC third corner
* @param VertexD fourth corner
* @return UV interpolation coordinates of the 'Point' in [0..1] range.
*/
inline FVector2D InvBilinear2DClamped(const FVector Point, const FVector VertexA, const FVector VertexB, const FVector VertexC, const FVector VertexD)
{
return InvBilinear2D(Point, VertexA, VertexB, VertexC, VertexD).ClampAxes(0.0, 1.0);
}
}; // UE::AI

405
ue_markdown.py Normal file
View File

@@ -0,0 +1,405 @@
"""
ue_markdown.py — Render a ParsedHeader into compact, agent-readable Markdown.
Format goals:
- Ultra-compact function entries: signature in heading, params folded into prose
- Bullet-list properties (no tables)
- Only items with actual C++ doc comments (or deprecated annotations) are emitted
- Cross-reference links to other files in the corpus (via type_index)
"""
from __future__ import annotations
import os
import re
from typing import Optional
from ue_parser import (
ParsedHeader, ClassInfo, EnumInfo, DelegateInfo, NamespaceInfo,
FreeFunction, FunctionInfo, PropertyInfo, DocComment, _split_params,
)
# ---------------------------------------------------------------------------
# Cross-reference utilities
# ---------------------------------------------------------------------------
def _rel_link(current_md: str, target_md: str) -> str:
"""
Compute a relative markdown link from the directory of current_md to target_md.
Both paths are relative to the docs root.
"""
cur_dir = os.path.dirname(current_md) or '.'
return os.path.relpath(target_md, cur_dir).replace('\\', '/')
def _make_type_link(name: str, type_index: dict[str, str],
current_md: str) -> str:
"""Return '[Name](relative/path.md)' if in corpus, else '`Name`'."""
if name in type_index:
link = _rel_link(current_md, type_index[name])
return f"[{name}]({link})"
return f"`{name}`"
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
def _has_doc(comment: Optional[DocComment]) -> bool:
if comment is None:
return False
return bool(comment.description or comment.params or comment.returns)
def _uf_category(uf_specs: str) -> str:
m = re.search(r'Category\s*=\s*"([^"]*)"', uf_specs)
if m:
return m.group(1)
m = re.search(r'Category\s*=\s*([\w|]+)', uf_specs)
return m.group(1) if m else ""
def _uf_flags(uf_specs: str) -> list:
flags = []
for flag in ('BlueprintCallable', 'BlueprintPure', 'BlueprintImplementableEvent',
'BlueprintNativeEvent', 'Exec', 'NetMulticast', 'Server', 'Client',
'CallInEditor'):
if re.search(r'\b' + flag + r'\b', uf_specs):
flags.append(flag)
return flags
def _up_flags(specs: str) -> list:
notable = [
'EditAnywhere', 'EditDefaultsOnly', 'EditInstanceOnly',
'VisibleAnywhere', 'VisibleDefaultsOnly',
'BlueprintReadWrite', 'BlueprintReadOnly', 'BlueprintAssignable',
'Replicated', 'ReplicatedUsing', 'Transient', 'SaveGame', 'Config',
]
return [f for f in notable if re.search(r'\b' + f + r'\b', specs)]
def _compact_params(raw_params: str, max_count: int = 3) -> str:
params = _split_params(raw_params)
if not params:
return ""
shown = []
for p in params[:max_count]:
p = p.strip()
p = re.sub(r'\s*=\s*', '=', p)
shown.append(p)
result = ', '.join(shown)
if len(params) > max_count:
result += ', ...'
return result
def _fn_flags_str(fn: FunctionInfo) -> str:
parts = []
if fn.is_deprecated:
ver = fn.deprecated_version or "?"
parts.append(f"Deprecated {ver}")
if fn.uf_specifiers:
uf = _uf_flags(fn.uf_specifiers)
cat = _uf_category(fn.uf_specifiers)
annotation = ', '.join(uf)
if cat:
annotation = (annotation + '' if annotation else '') + cat
if annotation:
parts.append(annotation)
for mod in fn.modifiers:
if mod in ('virtual', 'static', 'inline', 'constexpr'):
parts.append(mod)
if fn.editor_only:
parts.append("Editor only")
return f" *({', '.join(parts)})*" if parts else ""
_PLACEHOLDER_RE = re.compile(r'^[-_/\\]{6,}')
def _fn_body(fn: FunctionInfo) -> str:
parts = []
c = fn.comment
if c:
if c.description and not _PLACEHOLDER_RE.match(c.description):
desc = c.description.rstrip('.')
parts.append(desc + '.')
for pname, pdesc in c.params.items():
pdesc = pdesc.rstrip('.')
parts.append(f"`{pname}`: {pdesc}.")
if c.returns:
ret = c.returns.rstrip('.')
parts.append(f"**Returns** {ret}.")
return ' '.join(parts)
def _render_function_compact(fn: FunctionInfo) -> str:
params_str = _compact_params(fn.raw_params)
heading = f"##### `{fn.name}({params_str})`"
if fn.return_type:
heading += f" → `{fn.return_type}`"
heading += _fn_flags_str(fn)
body = _fn_body(fn)
return heading + ('\n' + body if body else '')
def _render_ff_compact(fn: FreeFunction, overloads: list = None) -> str:
overloads = overloads or [fn]
fn0 = overloads[0]
params_str = _compact_params(fn0.raw_params)
heading = f"##### `{fn0.name}({params_str})`"
if fn0.return_type:
heading += f" → `{fn0.return_type}`"
flag_parts = [m for m in fn0.modifiers if m in ('inline', 'static', 'constexpr')]
if len(overloads) > 1:
flag_parts.append(f"{len(overloads)} overloads")
if flag_parts:
heading += f" *({', '.join(flag_parts)})*"
body = ""
for f in overloads:
b = _fn_body(f)
if b:
body = b
break
return heading + ('\n' + body if body else '')
# ---------------------------------------------------------------------------
# Section renderers
# ---------------------------------------------------------------------------
def _render_delegates(delegates: list) -> str:
if not delegates:
return ""
lines = ["## Delegates"]
for d in delegates:
suffix_parts = []
if d.is_dynamic:
suffix_parts.append("Dynamic")
if d.is_multicast:
suffix_parts.append("Multicast")
suffix = ", ".join(suffix_parts)
head = f"### `{d.name}`"
if suffix:
head += f" *({suffix})*"
lines.append(head)
if d.comment and d.comment.description:
lines.append(d.comment.description)
if d.params:
param_str = ", ".join(f"`{t} {n}`" for t, n in d.params)
lines.append(f"Params: {param_str}")
return '\n'.join(lines)
def _render_enum(ei: EnumInfo) -> str:
head = f"### `{ei.name}`"
if ei.editor_only:
head += " *(Editor only)*"
lines = [head]
if ei.comment and ei.comment.description:
lines.append(ei.comment.description)
if ei.values:
has_descriptions = any(v.comment for v in ei.values)
if has_descriptions:
lines.append("| Value | Description |")
lines.append("|-------|-------------|")
for v in ei.values:
val_str = f"`{v.name}`"
if v.value:
val_str += f" (={v.value})"
lines.append(f"| {val_str} | {v.comment} |")
else:
# Compact inline list when no value has a description
vals = [v.name for v in ei.values]
lines.append("Values: " + ", ".join(f"`{v}`" for v in vals))
return '\n'.join(lines)
def _render_enums(enums: list, heading: str = "## Enums") -> str:
if not enums:
return ""
parts = [heading]
for ei in enums:
parts.append(_render_enum(ei))
return '\n'.join(parts)
def _render_properties(props: list) -> str:
visible = [p for p in props
if p.access in ('public', 'protected') and _has_doc(p.comment)]
if not visible:
return ""
lines = ["#### Properties"]
for p in visible:
flags = ', '.join(_up_flags(p.specifiers))
desc = p.comment.description if p.comment else ""
line = f"- `{p.name}` `{p.type}`"
if flags:
line += f" *({flags})*"
if desc:
line += f"{desc}"
if p.editor_only:
line += " *(Editor only)*"
lines.append(line)
return '\n'.join(lines)
def _render_functions(fns: list) -> str:
visible = [f for f in fns
if f.access in ('public', 'protected')
and _has_doc(f.comment)
and not f.is_deprecated]
if not visible:
return ""
lines = ["#### Functions"]
seen: dict[str, list] = {}
ordered: list[str] = []
for fn in visible:
if fn.name not in seen:
seen[fn.name] = []
ordered.append(fn.name)
seen[fn.name].append(fn)
for name in ordered:
group = seen[name]
if len(group) == 1:
lines.append(_render_function_compact(group[0]))
else:
fn0 = group[0]
params_str = _compact_params(fn0.raw_params)
heading = f"##### `{fn0.name}({params_str})`"
if fn0.return_type:
heading += f" → `{fn0.return_type}`"
flag_parts = []
if fn0.is_deprecated:
flag_parts.append(f"Deprecated {fn0.deprecated_version or '?'}")
if fn0.uf_specifiers:
uf = _uf_flags(fn0.uf_specifiers)
cat = _uf_category(fn0.uf_specifiers)
ann = ', '.join(uf)
if cat:
ann = (ann + '' if ann else '') + cat
if ann:
flag_parts.append(ann)
flag_parts.append(f"{len(group)} overloads")
heading += f" *({', '.join(flag_parts)})*"
lines.append(heading)
for fn in group:
body = _fn_body(fn)
if body:
lines.append(body)
break
return '\n'.join(lines)
def _class_has_content(ci: ClassInfo) -> bool:
if _has_doc(ci.comment):
return True
if any(p.access in ('public', 'protected') and _has_doc(p.comment)
for p in ci.properties):
return True
if any(f.access in ('public', 'protected') and (_has_doc(f.comment) or f.is_deprecated)
for f in ci.functions):
return True
if ci.nested_enums:
return True
return False
def _render_class(ci: ClassInfo,
type_index: dict[str, str], current_md: str) -> str:
lines = []
head = f"### `{ci.name}` *({ci.kind})*"
lines.append(head)
if ci.bases:
linked = [_make_type_link(b, type_index, current_md) for b in ci.bases]
lines.append("*Inherits*: " + ", ".join(linked))
if ci.module_api:
lines.append(f"*API*: `{ci.module_api}`")
if ci.comment and ci.comment.description:
lines.append(ci.comment.description)
if ci.nested_enums:
lines.append(_render_enums(ci.nested_enums, "#### Enums"))
props_section = _render_properties(ci.properties)
if props_section:
lines.append(props_section)
fns_section = _render_functions(ci.functions)
if fns_section:
lines.append(fns_section)
return '\n'.join(lines)
def _render_namespace(ns: NamespaceInfo) -> str:
doc_fns = [f for f in ns.functions if _has_doc(f.comment)]
if not doc_fns:
return ""
lines = [f"## Free Functions — `{ns.name}`"]
seen: dict[str, list] = {}
ordered: list[str] = []
for fn in doc_fns:
if fn.name not in seen:
seen[fn.name] = []
ordered.append(fn.name)
seen[fn.name].append(fn)
for name in ordered:
group = seen[name]
lines.append(_render_ff_compact(group[0], group))
return '\n'.join(lines)
# ---------------------------------------------------------------------------
# Main render function
# ---------------------------------------------------------------------------
def render_header(parsed: ParsedHeader,
type_index: dict[str, str] = None,
current_md: str = "") -> str:
if type_index is None:
type_index = {}
lines = []
lines.append(f"# `{parsed.filename}`")
lines.append(f"**Module**: `{parsed.module_name}`")
lines.append("")
sections = []
d_sec = _render_delegates(parsed.delegates)
if d_sec.strip():
sections.append(d_sec)
e_sec = _render_enums(parsed.enums)
if e_sec.strip():
sections.append(e_sec)
if parsed.classes:
doc_classes = [ci for ci in parsed.classes if _class_has_content(ci)]
if doc_classes:
cls_lines = ["## Classes"]
for ci in doc_classes:
cls_lines.append(_render_class(ci, type_index, current_md))
sections.append('\n'.join(cls_lines))
for ns in parsed.namespaces:
ns_sec = _render_namespace(ns)
if ns_sec.strip():
sections.append(ns_sec)
if parsed.free_functions:
doc_fns = [f for f in parsed.free_functions if _has_doc(f.comment)]
if doc_fns:
ff_lines = ["## Free Functions"]
for fn in doc_fns:
ff_lines.append(_render_ff_compact(fn))
sections.append('\n'.join(ff_lines))
lines.append('\n\n---\n\n'.join(sections))
return '\n'.join(lines)

1183
ue_parser.py Normal file

File diff suppressed because it is too large Load Diff