Replace ue-api skill with MCP server, support multi-directory input

- Add ue_mcp_server.py: 5 MCP tools (search_types, get_class_overview,
  get_member, get_file, search_source) for item-granularity doc lookups
- Add .mcp.json: registers server with Claude Code via stdio transport
- Remove .claude/skills/ue-api/SKILL.md: superseded by MCP tools
- Update generate.py: accept multiple input dirs/files, last arg is output;
  track per-file base path for correct relative output paths
- Remove samples/: headers deleted (corpus regenerated externally)
- Add possess_flow.png: sequence diagram of UE Possess flow with server/client split

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-27 08:20:04 -05:00
parent 904277e9e5
commit 241b246e9d
8 changed files with 271 additions and 1795 deletions

View File

@@ -1,109 +0,0 @@
---
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`

8
.mcp.json Normal file
View File

@@ -0,0 +1,8 @@
{
"mcpServers": {
"ue-docs": {
"command": "python",
"args": ["ue_mcp_server.py"]
}
}
}

View File

@@ -3,9 +3,10 @@
generate.py — CLI for UnrealDocGenerator. generate.py — CLI for UnrealDocGenerator.
Usage: Usage:
python generate.py <input> <output_dir> python generate.py <input> [input2 ...] <output_dir>
<input> can be a single .h file or a directory (processed recursively). Each <input> can be a single .h file or a directory (processed recursively).
The last argument is always the output directory.
Two-pass pipeline: Two-pass pipeline:
Pass 1 — parse every header, build a corpus-wide type index Pass 1 — parse every header, build a corpus-wide type index
@@ -20,24 +21,36 @@ from ue_parser import parse_header, ParsedHeader
from ue_markdown import render_header from ue_markdown import render_header
# ---------------------------------------------------------------------------
# Input collection
# ---------------------------------------------------------------------------
def collect_headers(input_arg: Path) -> list[tuple[Path, Path]]:
"""
Returns a list of (header_path, base_path) pairs for the given input.
base_path is used to compute relative output paths.
"""
if input_arg.is_file():
return [(input_arg, input_arg.parent)]
elif input_arg.is_dir():
return [(h, input_arg) for h in sorted(input_arg.rglob('*.h'))]
else:
print(f"Error: {input_arg} is not a file or directory", file=sys.stderr)
return []
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Type index # Type index
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
def build_type_index(parsed_list: list[tuple[Path, ParsedHeader]], def build_type_index(parsed_list: list[tuple[Path, Path, ParsedHeader]]) -> dict[str, str]:
input_base: Path) -> dict[str, str]:
""" """
Returns {TypeName: md_path_relative_to_docs_root} for every Returns {TypeName: md_path_relative_to_docs_root} for every
class, struct, enum, and delegate in the corpus. class, struct, enum, and delegate in the corpus.
""" """
index: dict[str, str] = {} index: dict[str, str] = {}
for h, parsed in parsed_list: for h, base, parsed in parsed_list:
try: md_rel = _md_rel(h, base)
rel = h.relative_to(input_base)
except ValueError:
rel = Path(h.name)
md_rel = str(rel.with_suffix('.md'))
for ci in parsed.classes: for ci in parsed.classes:
index[ci.name] = md_rel index[ci.name] = md_rel
for ei in parsed.enums: for ei in parsed.enums:
@@ -48,6 +61,15 @@ def build_type_index(parsed_list: list[tuple[Path, ParsedHeader]],
return index return index
def _md_rel(h: Path, base: Path) -> str:
"""Relative .md path for header h given its input base."""
try:
rel = h.relative_to(base)
except ValueError:
rel = Path(h.name)
return str(rel.with_suffix('.md'))
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Type index file # Type index file
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@@ -72,51 +94,43 @@ def write_type_index(type_index: dict[str, str], output_dir: Path) -> None:
def main(): def main():
if len(sys.argv) < 3: if len(sys.argv) < 3:
print("Usage: python generate.py <input> <output_dir>", file=sys.stderr) print("Usage: python generate.py <input> [input2 ...] <output_dir>", file=sys.stderr)
sys.exit(1) sys.exit(1)
input_arg = Path(sys.argv[1]) *input_args, output_arg = sys.argv[1:]
output_dir = Path(sys.argv[2]) output_dir = Path(output_arg)
output_dir.mkdir(parents=True, exist_ok=True) output_dir.mkdir(parents=True, exist_ok=True)
# Collect input files # Collect (header, base) pairs from all inputs
if input_arg.is_file(): header_pairs: list[tuple[Path, Path]] = []
headers = [input_arg] for arg in input_args:
input_base = input_arg.parent pairs = collect_headers(Path(arg))
elif input_arg.is_dir(): if not pairs:
headers = sorted(input_arg.rglob('*.h')) print(f"Warning: no .h files found in {arg}", file=sys.stderr)
input_base = input_arg header_pairs.extend(pairs)
else:
print(f"Error: {input_arg} is not a file or directory", file=sys.stderr)
sys.exit(1)
if not headers: if not header_pairs:
print("No .h files found.", file=sys.stderr) print("No .h files found.", file=sys.stderr)
sys.exit(1) sys.exit(1)
# --- Pass 1: parse all --- # --- Pass 1: parse all ---
parsed_list: list[tuple[Path, ParsedHeader]] = [] parsed_list: list[tuple[Path, Path, ParsedHeader]] = []
for h in headers: for h, base in header_pairs:
print(f"Parsing {h} ...") print(f"Parsing {h} ...")
try: try:
parsed = parse_header(str(h)) parsed = parse_header(str(h))
parsed_list.append((h, parsed)) parsed_list.append((h, base, parsed))
except Exception as exc: except Exception as exc:
print(f" ERROR parsing {h}: {exc}", file=sys.stderr) print(f" ERROR parsing {h}: {exc}", file=sys.stderr)
# --- Build corpus-wide type index --- # --- Build corpus-wide type index ---
type_index = build_type_index(parsed_list, input_base) type_index = build_type_index(parsed_list)
# --- Pass 2: render all --- # --- Pass 2: render all ---
success = 0 success = 0
for h, parsed in parsed_list: for h, base, parsed in parsed_list:
print(f"Rendering {h} ...") print(f"Rendering {h} ...")
try: current_md = _md_rel(h, base)
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 = output_dir / current_md
out_path.parent.mkdir(parents=True, exist_ok=True) out_path.parent.mkdir(parents=True, exist_ok=True)

BIN
possess_flow.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

View File

@@ -1,465 +0,0 @@
// 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

View File

@@ -1,175 +0,0 @@
// 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

213
ue_mcp_server.py Normal file
View File

@@ -0,0 +1,213 @@
"""MCP server exposing UE documentation tools at item granularity."""
import os
import re
import subprocess
from pathlib import Path
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("ue-docs")
# ---------------------------------------------------------------------------
# Env helpers
# ---------------------------------------------------------------------------
def _docs_root() -> Path:
p = os.environ.get("UE_DOCS_PATH", "")
if not p:
raise RuntimeError("UE_DOCS_PATH environment variable not set")
return Path(p)
def _engine_root() -> Path:
p = os.environ.get("UE_ENGINE_ROOT", "")
if not p:
raise RuntimeError("UE_ENGINE_ROOT environment variable not set")
return Path(p)
def _load_type_index() -> dict[str, str]:
"""Returns {TypeName: relative_md_path}."""
idx = (_docs_root() / "type-index.txt").read_text()
result = {}
for line in idx.splitlines():
if ": " in line:
name, path = line.split(": ", 1)
result[name.strip()] = path.strip()
return result
# ---------------------------------------------------------------------------
# Section extraction helpers
# ---------------------------------------------------------------------------
def _extract_class_section(text: str, class_name: str) -> str:
"""Return the markdown section for class_name (### `ClassName` … next ### `)."""
# Match the heading for this class
pattern = rf'^### `{re.escape(class_name)}`'
m = re.search(pattern, text, re.MULTILINE)
if not m:
return ""
start = m.start()
# Find the next same-level heading
next_h3 = re.search(r'^### `', text[m.end():], re.MULTILINE)
if next_h3:
end = m.end() + next_h3.start()
else:
end = len(text)
return text[start:end]
def _build_overview(section: str) -> str:
"""Compact overview: header block + name lists for Properties and Functions."""
lines = section.splitlines()
prop_names: list[str] = []
func_names: list[str] = [] # deduped
seen_funcs: set[str] = set()
header_lines: list[str] = []
in_props = False
in_funcs = False
for line in lines:
if line.startswith("#### Properties"):
in_props = True
in_funcs = False
continue
if line.startswith("#### Functions"):
in_funcs = True
in_props = False
continue
if line.startswith("#### ") or line.startswith("### "):
# Some other subsection — stop collecting
in_props = False
in_funcs = False
if in_props:
m = re.match(r"- `(\w+)`", line)
if m:
prop_names.append(m.group(1))
elif in_funcs:
m = re.match(r"##### `(\w+)\(", line)
if m:
name = m.group(1)
if name not in seen_funcs:
seen_funcs.add(name)
func_names.append(name)
else:
header_lines.append(line)
# Trim trailing blank lines from header
while header_lines and not header_lines[-1].strip():
header_lines.pop()
parts = ["\n".join(header_lines)]
if prop_names:
parts.append(f"\nProperties ({len(prop_names)}): {', '.join(prop_names)}")
if func_names:
parts.append(f"Functions ({len(func_names)}): {', '.join(func_names)}")
return "\n".join(parts)
def _extract_member(section: str, member_name: str) -> str:
"""Return the full doc entry for member_name (all overloads for functions)."""
escaped = re.escape(member_name)
# Try function first (##### `Name(`)
pattern = rf'^##### `{escaped}\('
m = re.search(pattern, section, re.MULTILINE)
if m:
start = m.start()
# Collect until the next ##### ` heading
next_h5 = re.search(r'^##### `', section[m.end():], re.MULTILINE)
if next_h5:
end = m.end() + next_h5.start()
else:
end = len(section)
return section[start:end].rstrip()
# Try property (- `Name`)
m = re.search(rf'^- `{escaped}`', section, re.MULTILINE)
if m:
return section[m.start():section.index("\n", m.start())].rstrip()
return ""
# ---------------------------------------------------------------------------
# Tools
# ---------------------------------------------------------------------------
@mcp.tool()
def search_types(pattern: str) -> str:
"""Search type-index.txt for UE types whose name matches pattern (case-insensitive).
Returns matching 'TypeName: path/to/File.md' lines (up to 50).
Use this to locate a type before calling get_class_overview or get_file."""
idx = _docs_root() / "type-index.txt"
lines = [l for l in idx.read_text().splitlines()
if re.search(pattern, l, re.IGNORECASE)]
return "\n".join(lines[:50]) or "(no matches)"
@mcp.tool()
def get_class_overview(class_name: str) -> str:
"""Compact overview of a UE class/struct: description, base classes,
and flat lists of property and function names. Use get_member() for
full signatures and docs. Use get_file() if you need delegates/enums too."""
index = _load_type_index()
if class_name not in index:
return f"'{class_name}' not in type index. Try search_types('{class_name}')."
text = (_docs_root() / index[class_name]).read_text()
section = _extract_class_section(text, class_name)
if not section:
return f"Section for '{class_name}' not found in {index[class_name]}."
return _build_overview(section)
@mcp.tool()
def get_member(class_name: str, member_name: str) -> str:
"""Full documentation entry for one function or property in a UE class.
For overloaded functions, returns all overloads together."""
index = _load_type_index()
if class_name not in index:
return f"'{class_name}' not found."
text = (_docs_root() / index[class_name]).read_text()
section = _extract_class_section(text, class_name)
if not section:
return f"'{class_name}' section missing."
entry = _extract_member(section, member_name)
return entry or f"'{member_name}' not found in {class_name}."
@mcp.tool()
def get_file(relative_path: str) -> str:
"""Full content of a documentation .md file.
relative_path is relative to $UE_DOCS_PATH, as returned by search_types().
Use this when you need delegates, top-level enums, or multiple classes from one file."""
path = _docs_root() / relative_path
if not path.exists():
return f"File not found: {relative_path}"
return path.read_text()
@mcp.tool()
def search_source(pattern: str, path_hint: str = "") -> str:
"""Grep UE source .h files for pattern. Requires $UE_ENGINE_ROOT.
path_hint: optional subdirectory under Engine/Source/ (e.g. 'Runtime/Engine').
Returns up to 40 matching lines with file:line context."""
root = _engine_root() / "Engine" / "Source"
if path_hint:
root = root / path_hint
result = subprocess.run(
["grep", "-rn", "--include=*.h", "-m", "40", pattern, str(root)],
capture_output=True, text=True, timeout=15
)
return result.stdout or "(no matches)"
if __name__ == "__main__":
mcp.run()