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:
@@ -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
8
.mcp.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"mcpServers": {
|
||||
"ue-docs": {
|
||||
"command": "python",
|
||||
"args": ["ue_mcp_server.py"]
|
||||
}
|
||||
}
|
||||
}
|
||||
86
generate.py
86
generate.py
@@ -3,9 +3,10 @@
|
||||
generate.py — CLI for UnrealDocGenerator.
|
||||
|
||||
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:
|
||||
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
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 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
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def build_type_index(parsed_list: list[tuple[Path, ParsedHeader]],
|
||||
input_base: Path) -> dict[str, str]:
|
||||
def build_type_index(parsed_list: list[tuple[Path, Path, ParsedHeader]]) -> 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 h, base, parsed in parsed_list:
|
||||
md_rel = _md_rel(h, base)
|
||||
for ci in parsed.classes:
|
||||
index[ci.name] = md_rel
|
||||
for ei in parsed.enums:
|
||||
@@ -48,6 +61,15 @@ def build_type_index(parsed_list: list[tuple[Path, ParsedHeader]],
|
||||
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
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -72,51 +94,43 @@ def write_type_index(type_index: dict[str, str], output_dir: Path) -> None:
|
||||
|
||||
def main():
|
||||
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)
|
||||
|
||||
input_arg = Path(sys.argv[1])
|
||||
output_dir = Path(sys.argv[2])
|
||||
*input_args, output_arg = sys.argv[1:]
|
||||
output_dir = Path(output_arg)
|
||||
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)
|
||||
# Collect (header, base) pairs from all inputs
|
||||
header_pairs: list[tuple[Path, Path]] = []
|
||||
for arg in input_args:
|
||||
pairs = collect_headers(Path(arg))
|
||||
if not pairs:
|
||||
print(f"Warning: no .h files found in {arg}", file=sys.stderr)
|
||||
header_pairs.extend(pairs)
|
||||
|
||||
if not headers:
|
||||
if not header_pairs:
|
||||
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:
|
||||
parsed_list: list[tuple[Path, Path, ParsedHeader]] = []
|
||||
for h, base in header_pairs:
|
||||
print(f"Parsing {h} ...")
|
||||
try:
|
||||
parsed = parse_header(str(h))
|
||||
parsed_list.append((h, parsed))
|
||||
parsed_list.append((h, base, 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)
|
||||
type_index = build_type_index(parsed_list)
|
||||
|
||||
# --- Pass 2: render all ---
|
||||
success = 0
|
||||
for h, parsed in parsed_list:
|
||||
for h, base, 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'))
|
||||
current_md = _md_rel(h, base)
|
||||
out_path = output_dir / current_md
|
||||
out_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
|
||||
BIN
possess_flow.png
Normal file
BIN
possess_flow.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 80 KiB |
@@ -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
@@ -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
213
ue_mcp_server.py
Normal 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()
|
||||
Reference in New Issue
Block a user