From 241b246e9d31f1427eb574be22a577b2dd660be8 Mon Sep 17 00:00:00 2001 From: Pierre-Marie Charavel Date: Fri, 27 Feb 2026 08:20:04 -0500 Subject: [PATCH] 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 --- .claude/skills/ue-api/SKILL.md | 109 ---- .mcp.json | 8 + generate.py | 86 +-- possess_flow.png | Bin 0 -> 81768 bytes samples/AIController.h | 465 --------------- samples/GameplayTagsManager.h | 1010 -------------------------------- samples/GeomUtils.h | 175 ------ ue_mcp_server.py | 213 +++++++ 8 files changed, 271 insertions(+), 1795 deletions(-) delete mode 100644 .claude/skills/ue-api/SKILL.md create mode 100644 .mcp.json create mode 100644 possess_flow.png delete mode 100644 samples/AIController.h delete mode 100644 samples/GameplayTagsManager.h delete mode 100644 samples/GeomUtils.h create mode 100644 ue_mcp_server.py diff --git a/.claude/skills/ue-api/SKILL.md b/.claude/skills/ue-api/SKILL.md deleted file mode 100644 index fcf21b1..0000000 --- a/.claude/skills/ue-api/SKILL.md +++ /dev/null @@ -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` diff --git a/.mcp.json b/.mcp.json new file mode 100644 index 0000000..89193fc --- /dev/null +++ b/.mcp.json @@ -0,0 +1,8 @@ +{ + "mcpServers": { + "ue-docs": { + "command": "python", + "args": ["ue_mcp_server.py"] + } + } +} diff --git a/generate.py b/generate.py index 9ff5b2e..c068cf5 100644 --- a/generate.py +++ b/generate.py @@ -3,9 +3,10 @@ generate.py — CLI for UnrealDocGenerator. Usage: - python generate.py + python generate.py [input2 ...] - can be a single .h file or a directory (processed recursively). +Each 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 ", file=sys.stderr) + print("Usage: python generate.py [input2 ...] ", 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) diff --git a/possess_flow.png b/possess_flow.png new file mode 100644 index 0000000000000000000000000000000000000000..8af0f2ec5ce415d723d33a75a1d677cb27804366 GIT binary patch literal 81768 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 literal 0 HcmV?d00001 diff --git a/samples/AIController.h b/samples/AIController.h deleted file mode 100644 index cb74dad..0000000 --- a/samples/AIController.h +++ /dev/null @@ -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 Actor; - FVector Position; - - FFocusItem() - { - Actor = nullptr; - Position = FAISystem::InvalidLocation; - } - }; - - TArray 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 PathFollowingComponent; - -public: - - /** Component responsible for behaviors. */ - UPROPERTY(BlueprintReadWrite, Category = AI) - TObjectPtr BrainComponent; - - UPROPERTY(VisibleDefaultsOnly, Category = AI) - TObjectPtr PerceptionComponent; - -protected: - /** blackboard */ - UPROPERTY(BlueprintReadOnly, Category = AI, meta = (AllowPrivateAccess = "true")) - TObjectPtr Blackboard; - - UPROPERTY() - TObjectPtr CachedGameplayTasksComponent; - - UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = AI) - TSubclassOf 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 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 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 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 ResourceClass); - - UFUNCTION(BlueprintCallable, Category = "AI|Tasks") - AIMODULE_API void UnclaimTaskResource(TSubclassOf 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& 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(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; - } -} diff --git a/samples/GameplayTagsManager.h b/samples/GameplayTagsManager.h deleted file mode 100644 index 68eb7c9..0000000 --- a/samples/GameplayTagsManager.h +++ /dev/null @@ -1,1010 +0,0 @@ -// Copyright Epic Games, Inc. All Rights Reserved. - -#pragma once - -#include "AssetRegistry/AssetData.h" -#include "CoreMinimal.h" -#include "Stats/Stats.h" -#include "UObject/ObjectMacros.h" -#include "UObject/Object.h" -#include "UObject/ScriptMacros.h" -#include "GameplayTagContainer.h" -#include "Engine/DataTable.h" -#include "Templates/UniquePtr.h" -#include "Misc/ScopeLock.h" -#include "Misc/TransactionallySafeCriticalSection.h" -#if WITH_EDITOR -#include "Hash/Blake3.h" -#endif - -#include "GameplayTagsManager.generated.h" - -class UGameplayTagsList; -struct FStreamableHandle; -class FNativeGameplayTag; - -#if WITH_EDITOR -namespace UE::Cook { class FCookDependency; } -namespace UE::Cook { class ICookInfo; } -#endif - -/** Simple struct for a table row in the gameplay tag table and element in the ini list */ -USTRUCT() -struct FGameplayTagTableRow : public FTableRowBase -{ - GENERATED_USTRUCT_BODY() - - /** Tag specified in the table */ - UPROPERTY(EditAnywhere, BlueprintReadOnly, Category=GameplayTag) - FName Tag; - - /** Developer comment clarifying the usage of a particular tag, not user facing */ - UPROPERTY(EditAnywhere, BlueprintReadOnly, Category=GameplayTag) - FString DevComment; - - /** Constructors */ - FGameplayTagTableRow() {} - FGameplayTagTableRow(FName InTag, const FString& InDevComment = TEXT("")) : Tag(InTag), DevComment(InDevComment) {} - GAMEPLAYTAGS_API FGameplayTagTableRow(FGameplayTagTableRow const& Other); - - /** Assignment/Equality operators */ - GAMEPLAYTAGS_API FGameplayTagTableRow& operator=(FGameplayTagTableRow const& Other); - GAMEPLAYTAGS_API bool operator==(FGameplayTagTableRow const& Other) const; - GAMEPLAYTAGS_API bool operator!=(FGameplayTagTableRow const& Other) const; - GAMEPLAYTAGS_API bool operator<(FGameplayTagTableRow const& Other) const; -}; - -/** Simple struct for a table row in the restricted gameplay tag table and element in the ini list */ -USTRUCT() -struct FRestrictedGameplayTagTableRow : public FGameplayTagTableRow -{ - GENERATED_USTRUCT_BODY() - - /** Tag specified in the table */ - UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = GameplayTag) - bool bAllowNonRestrictedChildren; - - /** Constructors */ - FRestrictedGameplayTagTableRow() : bAllowNonRestrictedChildren(false) {} - FRestrictedGameplayTagTableRow(FName InTag, const FString& InDevComment = TEXT(""), bool InAllowNonRestrictedChildren = false) : FGameplayTagTableRow(InTag, InDevComment), bAllowNonRestrictedChildren(InAllowNonRestrictedChildren) {} - GAMEPLAYTAGS_API FRestrictedGameplayTagTableRow(FRestrictedGameplayTagTableRow const& Other); - - /** Assignment/Equality operators */ - GAMEPLAYTAGS_API FRestrictedGameplayTagTableRow& operator=(FRestrictedGameplayTagTableRow const& Other); - GAMEPLAYTAGS_API bool operator==(FRestrictedGameplayTagTableRow const& Other) const; - GAMEPLAYTAGS_API bool operator!=(FRestrictedGameplayTagTableRow const& Other) const; -}; - -UENUM() -enum class EGameplayTagSourceType : uint8 -{ - Native, // Was added from C++ code - DefaultTagList, // The default tag list in DefaultGameplayTags.ini - TagList, // Another tag list from an ini in tags/*.ini - RestrictedTagList, // Restricted tags from an ini - DataTable, // From a DataTable - Invalid, // Not a real source -}; - -UENUM() -enum class EGameplayTagSelectionType : uint8 -{ - None, - NonRestrictedOnly, - RestrictedOnly, - All -}; - -/** Struct defining where gameplay tags are loaded/saved from. Mostly for the editor */ -USTRUCT() -struct FGameplayTagSource -{ - GENERATED_USTRUCT_BODY() - - /** Name of this source */ - UPROPERTY() - FName SourceName; - - /** Type of this source */ - UPROPERTY() - EGameplayTagSourceType SourceType; - - /** If this is bound to an ini object for saving, this is the one */ - UPROPERTY() - TObjectPtr SourceTagList; - - /** If this has restricted tags and is bound to an ini object for saving, this is the one */ - UPROPERTY() - TObjectPtr SourceRestrictedTagList; - - FGameplayTagSource() - : SourceName(NAME_None), SourceType(EGameplayTagSourceType::Invalid), SourceTagList(nullptr), SourceRestrictedTagList(nullptr) - { - } - - FGameplayTagSource(FName InSourceName, EGameplayTagSourceType InSourceType, UGameplayTagsList* InSourceTagList = nullptr, URestrictedGameplayTagsList* InSourceRestrictedTagList = nullptr) - : SourceName(InSourceName), SourceType(InSourceType), SourceTagList(InSourceTagList), SourceRestrictedTagList(InSourceRestrictedTagList) - { - } - - /** Returns the config file that created this source, if valid */ - GAMEPLAYTAGS_API FString GetConfigFileName() const; - - static GAMEPLAYTAGS_API FName GetNativeName(); - - static GAMEPLAYTAGS_API FName GetDefaultName(); - -#if WITH_EDITOR - static GAMEPLAYTAGS_API FName GetFavoriteName(); - - static GAMEPLAYTAGS_API void SetFavoriteName(FName TagSourceToFavorite); - - static GAMEPLAYTAGS_API FName GetTransientEditorName(); -#endif -}; - -/** Struct describing the places to look for ini search paths */ -struct FGameplayTagSearchPathInfo -{ - /** Which sources should be loaded from this path */ - TArray SourcesInPath; - - /** Config files to load from, will normally correspond to FoundSources */ - TArray TagIniList; - - /** True if this path has already been searched */ - bool bWasSearched = false; - - /** True if the tags in sources have been added to the current tree */ - bool bWasAddedToTree = false; - - inline void Reset() - { - SourcesInPath.Reset(); - TagIniList.Reset(); - bWasSearched = false; - bWasAddedToTree = false; - } - - inline bool IsValid() - { - return bWasSearched && bWasAddedToTree; - } -}; - -/** Simple tree node for gameplay tags, this stores metadata about specific tags */ -USTRUCT() -struct FGameplayTagNode -{ - GENERATED_USTRUCT_BODY() - FGameplayTagNode(){}; - - /** Simple constructor, passing redundant data for performance */ - FGameplayTagNode(FName InTag, FName InFullTag, TSharedPtr InParentNode, bool InIsExplicitTag, bool InIsRestrictedTag, bool InAllowNonRestrictedChildren); - - /** Returns a correctly constructed container with only this tag, useful for doing container queries */ - inline const FGameplayTagContainer& GetSingleTagContainer() const { return CompleteTagWithParents; } - - /** - * Get the complete tag for the node, including all parent tags, delimited by periods - * - * @return Complete tag for the node - */ - inline const FGameplayTag& GetCompleteTag() const { return CompleteTagWithParents.Num() > 0 ? CompleteTagWithParents.GameplayTags[0] : FGameplayTag::EmptyTag; } - inline FName GetCompleteTagName() const { return GetCompleteTag().GetTagName(); } - inline FString GetCompleteTagString() const { return GetCompleteTag().ToString(); } - - /** - * Get the simple tag for the node (doesn't include any parent tags) - * - * @return Simple tag for the node - */ - inline FName GetSimpleTagName() const { return Tag; } - - /** - * Get the children nodes of this node - * - * @return Reference to the array of the children nodes of this node - */ - inline TArray< TSharedPtr >& GetChildTagNodes() { return ChildTags; } - - /** - * Get the children nodes of this node - * - * @return Reference to the array of the children nodes of this node - */ - inline const TArray< TSharedPtr >& GetChildTagNodes() const { return ChildTags; } - - /** - * Get the parent tag node of this node - * - * @return The parent tag node of this node - */ - inline TSharedPtr GetParentTagNode() const { return ParentNode; } - - /** - * Get the net index of this node - * - * @return The net index of this node - */ - inline FGameplayTagNetIndex GetNetIndex() const { check(NetIndex != INVALID_TAGNETINDEX); return NetIndex; } - - /** Reset the node of all of its values */ - GAMEPLAYTAGS_API void ResetNode(); - - /** Returns true if the tag was explicitly specified in code or data */ - inline bool IsExplicitTag() const { -#if WITH_EDITORONLY_DATA - return bIsExplicitTag; -#else - return true; -#endif - } - - /** Returns true if the tag is a restricted tag and allows non-restricted children */ - inline bool GetAllowNonRestrictedChildren() const { -#if WITH_EDITORONLY_DATA - return bAllowNonRestrictedChildren; -#else - return true; -#endif - } - - /** Returns true if the tag is a restricted tag */ - inline bool IsRestrictedGameplayTag() const { -#if WITH_EDITORONLY_DATA - return bIsRestrictedTag; -#else - return true; -#endif - } - -#if WITH_EDITORONLY_DATA - FName GetFirstSourceName() const { return SourceNames.Num() == 0 ? NAME_None : SourceNames[0]; } - const TArray& GetAllSourceNames() const { return SourceNames; } -#endif - -#if WITH_EDITORONLY_DATA - /** Returns the Comment for this tag */ - FString GetDevComment() const { return DevComment; } -#endif - -#if WITH_EDITOR - /** - * Update the hasher with a deterministic hash of the data on this. Used for e.g. IncrementalCook keys. - * Does not include data from this node's child or parent nodes. - */ - GAMEPLAYTAGS_API void Hash(FBlake3& Hasher); -#endif - -private: - /** Raw name for this tag at current rank in the tree */ - FName Tag; - - /** This complete tag is at GameplayTags[0], with parents in ParentTags[] */ - FGameplayTagContainer CompleteTagWithParents; - - /** Child gameplay tag nodes */ - TArray< TSharedPtr > ChildTags; - - /** Owner gameplay tag node, if any */ - TSharedPtr ParentNode; - - /** Net Index of this node */ - FGameplayTagNetIndex NetIndex; - -#if WITH_EDITORONLY_DATA - /** Module or Package or config file this tag came from. If empty this is an implicitly added tag */ - TArray SourceNames; - - /** Comment for this tag */ - FString DevComment; - - /** If this is true then the tag can only have normal tag children if bAllowNonRestrictedChildren is true */ - uint8 bIsRestrictedTag : 1; - - /** If this is true then any children of this tag must come from the restricted tags */ - uint8 bAllowNonRestrictedChildren : 1; - - /** If this is true then the tag was explicitly added and not only implied by its child tags */ - uint8 bIsExplicitTag : 1; - - /** If this is true then at least one tag that inherits from this tag is coming from multiple sources. Used for updating UI in the editor. */ - uint8 bDescendantHasConflict : 1; - - /** If this is true then this tag is coming from multiple sources. No descendants can be changed on this tag until this is resolved. */ - uint8 bNodeHasConflict : 1; - - /** If this is true then at least one tag that this tag descends from is coming from multiple sources. This tag and it's descendants can't be changed in the editor. */ - uint8 bAncestorHasConflict : 1; -#endif - - friend class UGameplayTagsManager; - friend class SGameplayTagWidget; - friend class SGameplayTagPicker; -}; - -/** Holds data about the tag dictionary, is in a singleton UObject */ -UCLASS(config=Engine, MinimalAPI) -class UGameplayTagsManager : public UObject -{ - GENERATED_UCLASS_BODY() - - /** Destructor */ - GAMEPLAYTAGS_API ~UGameplayTagsManager(); - - /** Returns the global UGameplayTagsManager manager */ - inline static UGameplayTagsManager& Get() - { - if (SingletonManager == nullptr) - { - InitializeManager(); - } - - return *SingletonManager; - } - - /** Returns possibly nullptr to the manager. Needed for some shutdown cases to avoid reallocating. */ - inline static UGameplayTagsManager* GetIfAllocated() { return SingletonManager; } - - /** - * Adds the gameplay tags corresponding to the strings in the array TagStrings to OutTagsContainer - * - * @param TagStrings Array of strings to search for as tags to add to the tag container - * @param OutTagsContainer Container to add the found tags to. - * @param ErrorIfNotfound: ensure() that tags exists. - * - */ - GAMEPLAYTAGS_API void RequestGameplayTagContainer(const TArray& TagStrings, FGameplayTagContainer& OutTagsContainer, bool bErrorIfNotFound=true) const; - - /** - * Gets the FGameplayTag that corresponds to the TagName - * - * @param TagName The Name of the tag to search for - * @param ErrorIfNotfound: ensure() that tag exists. - * - * @return Will return the corresponding FGameplayTag or an empty one if not found. - */ - GAMEPLAYTAGS_API FGameplayTag RequestGameplayTag(FName TagName, bool ErrorIfNotFound=true) const; - - /** - * Returns true if this is a valid gameplay tag string (foo.bar.baz). If false, it will fill - * @param TagString String to check for validity - * @param OutError If non-null and string invalid, will fill in with an error message - * @param OutFixedString If non-null and string invalid, will attempt to fix. Will be empty if no fix is possible - * @return True if this can be added to the tag dictionary, false if there's a syntax error - */ - GAMEPLAYTAGS_API bool IsValidGameplayTagString(const TCHAR* TagString, FText* OutError = nullptr, FString* OutFixedString = nullptr); - GAMEPLAYTAGS_API bool IsValidGameplayTagString(const FString& TagString, FText* OutError = nullptr, FString* OutFixedString = nullptr); - GAMEPLAYTAGS_API bool IsValidGameplayTagString(const FStringView& TagString, FText* OutError = nullptr, FStringBuilderBase* OutFixedString = nullptr); - - /** - * Searches for a gameplay tag given a partial string. This is slow and intended mainly for console commands/utilities to make - * developer life's easier. This will attempt to match as best as it can. If you pass "A.b" it will match on "A.b." before it matches "a.b.c". - */ - GAMEPLAYTAGS_API FGameplayTag FindGameplayTagFromPartialString_Slow(FString PartialString) const; - - /** - * Registers the given name as a gameplay tag, and tracks that it is being directly referenced from code - * This can only be called during engine initialization, the table needs to be locked down before replication - * - * @param TagName The Name of the tag to add - * @param TagDevComment The developer comment clarifying the usage of the tag - * - * @return Will return the corresponding FGameplayTag - */ - GAMEPLAYTAGS_API FGameplayTag AddNativeGameplayTag(FName TagName, const FString& TagDevComment = TEXT("(Native)")); - -private: - // Only callable from FNativeGameplayTag, these functions do less error checking and can happen after initial tag loading is done - GAMEPLAYTAGS_API void AddNativeGameplayTag(FNativeGameplayTag* TagSource); - GAMEPLAYTAGS_API void RemoveNativeGameplayTag(const FNativeGameplayTag* TagSource); - -public: - /** Call to flush the list of native tags, once called it is unsafe to add more */ - GAMEPLAYTAGS_API void DoneAddingNativeTags(); - - /** This is a delegate that is called during initialization/initial loading and signals the last chance to add tags before we are considered to be fully loaded (all tags registered). */ - static GAMEPLAYTAGS_API FSimpleMulticastDelegate& OnLastChanceToAddNativeTags(); - - /** - * Register a callback for when native tags are done being added (this is also a safe point to consider that the gameplay tags have fully been initialized). - * Or, if the native tags have already been added (and thus we have registered all valid tags), then execute this Delegate immediately. - * This is useful if your code is potentially executed during load time, and therefore any tags in your block of code could be not-yet-loaded, but possibly valid after being loaded. - */ - GAMEPLAYTAGS_API FDelegateHandle CallOrRegister_OnDoneAddingNativeTagsDelegate(const FSimpleMulticastDelegate::FDelegate& Delegate) const; - - /** - * Gets a Tag Container containing the supplied tag and all of its parents as explicit tags. - * For example, passing in x.y.z would return a tag container with x.y.z, x.y, and x. - * This will only work for tags that have been properly registered. - * - * @param GameplayTag The tag to use at the child most tag for this container - * - * @return A tag container with the supplied tag and all its parents added explicitly, or an empty container if that failed - */ - GAMEPLAYTAGS_API FGameplayTagContainer RequestGameplayTagParents(const FGameplayTag& GameplayTag) const; - - /** - * Fills in an array of gameplay tags with all of tags that are the parents of the passed in tag. - * For example, passing in x.y.z would add x.y and x to UniqueParentTags if they was not already there. - * This is used by the GameplayTagContainer code and may work for unregistered tags depending on serialization settings. - * - * @param GameplayTag The gameplay tag to extract parent tags from - * @param UniqueParentTags A list of parent tags that will be added to if necessary - * - * @return true if any tags were added to UniqueParentTags - */ - GAMEPLAYTAGS_API bool ExtractParentTags(const FGameplayTag& GameplayTag, TArray& UniqueParentTags) const; - - /** - * Gets a Tag Container containing the all tags in the hierarchy that are children of this tag. Does not return the original tag - * - * @param GameplayTag The Tag to use at the parent tag - * - * @return A Tag Container with the supplied tag and all its parents added explicitly - */ - GAMEPLAYTAGS_API FGameplayTagContainer RequestGameplayTagChildren(const FGameplayTag& GameplayTag) const; - - /** Returns direct parent GameplayTag of this GameplayTag, calling on x.y will return x */ - GAMEPLAYTAGS_API FGameplayTag RequestGameplayTagDirectParent(const FGameplayTag& GameplayTag) const; - - UE_DEPRECATED(5.4, "This function is not threadsafe, use FindTagNode or FGameplayTag::GetSingleTagContainer") - inline const FGameplayTagContainer* GetSingleTagContainer(const FGameplayTag& GameplayTag) const - { - return GetSingleTagContainerPtr(GameplayTag); - } - - /** - * Checks node tree to see if a FGameplayTagNode with the tag exists - * - * @param TagName The name of the tag node to search for - * - * @return A shared pointer to the FGameplayTagNode found, or NULL if not found. - */ - inline TSharedPtr FindTagNode(const FGameplayTag& GameplayTag) const - { - UE::TScopeLock Lock(GameplayTagMapCritical); - - const TSharedPtr* Node = GameplayTagNodeMap.Find(GameplayTag); - - if (Node) - { - return *Node; - } -#if WITH_EDITOR - // Check redirector - if (GIsEditor && GameplayTag.IsValid()) - { - FGameplayTag RedirectedTag = GameplayTag; - - RedirectSingleGameplayTag(RedirectedTag, nullptr); - - Node = GameplayTagNodeMap.Find(RedirectedTag); - - if (Node) - { - return *Node; - } - } -#endif - return nullptr; - } - - /** - * Checks node tree to see if a FGameplayTagNode with the name exists - * - * @param TagName The name of the tag node to search for - * - * @return A shared pointer to the FGameplayTagNode found, or NULL if not found. - */ - inline TSharedPtr FindTagNode(FName TagName) const - { - FGameplayTag PossibleTag(TagName); - return FindTagNode(PossibleTag); - } - - /** Loads the tag tables referenced in the GameplayTagSettings object */ - GAMEPLAYTAGS_API void LoadGameplayTagTables(bool bAllowAsyncLoad = false); - - /** Loads tag inis contained in the specified path, passes an optional PluginConfigCache to speed up disk searches */ - GAMEPLAYTAGS_API void AddTagIniSearchPath(const FString& RootDir, const TSet* PluginConfigsCache = nullptr); - - /** Tries to remove the specified search path, will return true if anything was removed */ - GAMEPLAYTAGS_API bool RemoveTagIniSearchPath(const FString& RootDir); - - /** Gets all the current directories to look for tag sources in */ - GAMEPLAYTAGS_API void GetTagSourceSearchPaths(TArray& OutPaths); - - /** Gets the number of tag source search paths */ - GAMEPLAYTAGS_API int32 GetNumTagSourceSearchPaths(); - - /** Helper function to construct the gameplay tag tree */ - GAMEPLAYTAGS_API void ConstructGameplayTagTree(); - - /** Helper function to destroy the gameplay tag tree */ - GAMEPLAYTAGS_API void DestroyGameplayTagTree(); - - /** Splits a tag such as x.y.z into an array of names {x,y,z} */ - GAMEPLAYTAGS_API void SplitGameplayTagFName(const FGameplayTag& Tag, TArray& OutNames) const; - - /** Gets the list of all registered tags, setting OnlyIncludeDictionaryTags will exclude implicitly added tags if possible */ - GAMEPLAYTAGS_API void RequestAllGameplayTags(FGameplayTagContainer& TagContainer, bool OnlyIncludeDictionaryTags) const; - - /** Returns true if if the passed in name is in the tag dictionary and can be created */ - GAMEPLAYTAGS_API bool ValidateTagCreation(FName TagName) const; - - /** Returns the tag source for a given tag source name and type, or null if not found */ - GAMEPLAYTAGS_API const FGameplayTagSource* FindTagSource(FName TagSourceName) const; - - /** Returns the tag source for a given tag source name and type, or null if not found */ - GAMEPLAYTAGS_API FGameplayTagSource* FindTagSource(FName TagSourceName); - - /** Fills in an array with all tag sources of a specific type */ - GAMEPLAYTAGS_API void FindTagSourcesWithType(EGameplayTagSourceType TagSourceType, TArray& OutArray) const; - - GAMEPLAYTAGS_API void FindTagsWithSource(FStringView PackageNameOrPath, TArray& OutTags) const; - - /** - * Check to see how closely two FGameplayTags match. Higher values indicate more matching terms in the tags. - * - * @param GameplayTagOne The first tag to compare - * @param GameplayTagTwo The second tag to compare - * - * @return the length of the longest matching pair - */ - GAMEPLAYTAGS_API int32 GameplayTagsMatchDepth(const FGameplayTag& GameplayTagOne, const FGameplayTag& GameplayTagTwo) const; - - /** Returns the number of parents a particular gameplay tag has. Useful as a quick way to determine which tags may - * be more "specific" than other tags without comparing whether they are in the same hierarchy or anything else. - * Example: "TagA.SubTagA" has 2 Tag Nodes. "TagA.SubTagA.LeafTagA" has 3 Tag Nodes. - */ - GAMEPLAYTAGS_API int32 GetNumberOfTagNodes(const FGameplayTag& GameplayTag) const; - - /** Returns true if we should import tags from UGameplayTagsSettings objects (configured by INI files) */ - GAMEPLAYTAGS_API bool ShouldImportTagsFromINI() const; - - /** Should we print loading errors when trying to load invalid tags */ - bool ShouldWarnOnInvalidTags() const - { - return bShouldWarnOnInvalidTags; - } - - /** Should we clear references to invalid tags loaded/saved in the editor */ - UE_DEPRECATED(5.5, "We should never clear invalid tags as we're not guaranteed the required plugin has loaded") - bool ShouldClearInvalidTags() const - { - return false; - } - - /** Should use fast replication */ - bool ShouldUseFastReplication() const - { - return bUseFastReplication; - } - - /** Should use dynamic replication (Gameplay Tags need not match between client/server) */ - bool ShouldUseDynamicReplication() const - { - return !bUseFastReplication && bUseDynamicReplication; - } - - /** If we are allowed to unload tags */ - GAMEPLAYTAGS_API bool ShouldUnloadTags() const; - - /** Pushes an override that supersedes bShouldAllowUnloadingTags to allow/disallow unloading of GameplayTags in controlled scenarios */ - GAMEPLAYTAGS_API void SetShouldUnloadTagsOverride(bool bShouldUnloadTags); - - /** Clears runtime overrides, reverting to bShouldAllowUnloadingTags when determining GameplayTags unload behavior */ - GAMEPLAYTAGS_API void ClearShouldUnloadTagsOverride(); - - /** Pushes an override that suppresses calls to HandleGameplayTagTreeChanged that would result in a complete rebuild of the GameplayTag tree */ - GAMEPLAYTAGS_API void SetShouldDeferGameplayTagTreeRebuilds(bool bShouldDeferRebuilds); - - /** Stops suppressing GameplayTag tree rebuilds and (optionally) rebuilds the tree */ - GAMEPLAYTAGS_API void ClearShouldDeferGameplayTagTreeRebuilds(bool bRebuildTree); - - /** Returns the hash of NetworkGameplayTagNodeIndex */ - uint32 GetNetworkGameplayTagNodeIndexHash() const { VerifyNetworkIndex(); return NetworkGameplayTagNodeIndexHash; } - - /** Returns a list of the ini files that contain restricted tags */ - GAMEPLAYTAGS_API void GetRestrictedTagConfigFiles(TArray& RestrictedConfigFiles) const; - - /** Returns a list of the source files that contain restricted tags */ - GAMEPLAYTAGS_API void GetRestrictedTagSources(TArray& Sources) const; - - /** Returns a list of the owners for a restricted tag config file. May be empty */ - GAMEPLAYTAGS_API void GetOwnersForTagSource(const FString& SourceName, TArray& OutOwners) const; - - /** Notification that a tag container has been loaded via serialize */ - GAMEPLAYTAGS_API void GameplayTagContainerLoaded(FGameplayTagContainer& Container, FProperty* SerializingProperty) const; - - /** Notification that a gameplay tag has been loaded via serialize */ - GAMEPLAYTAGS_API void SingleGameplayTagLoaded(FGameplayTag& Tag, FProperty* SerializingProperty) const; - - /** Handles redirectors for an entire container, will also error on invalid tags */ - GAMEPLAYTAGS_API void RedirectTagsForContainer(FGameplayTagContainer& Container, FProperty* SerializingProperty) const; - - /** Handles redirectors for a single tag, will also error on invalid tag. This is only called for when individual tags are serialized on their own */ - GAMEPLAYTAGS_API void RedirectSingleGameplayTag(FGameplayTag& Tag, FProperty* SerializingProperty) const; - - /** Handles establishing a single tag from an imported tag name (accounts for redirects too). Called when tags are imported via text. */ - GAMEPLAYTAGS_API bool ImportSingleGameplayTag(FGameplayTag& Tag, FName ImportedTagName, bool bImportFromSerialize = false) const; - - /** Gets a tag name from net index and vice versa, used for replication efficiency */ - GAMEPLAYTAGS_API FName GetTagNameFromNetIndex(FGameplayTagNetIndex Index) const; - GAMEPLAYTAGS_API FGameplayTagNetIndex GetNetIndexFromTag(const FGameplayTag &InTag) const; - - /** Cached number of bits we need to replicate tags. That is, Log2(Number of Tags). Will always be <= 16. */ - int32 GetNetIndexTrueBitNum() const { VerifyNetworkIndex(); return NetIndexTrueBitNum; } - - /** The length in bits of the first segment when net serializing tags. We will serialize NetIndexFirstBitSegment + 1 bit to indicatore "more" (more = second segment that is NetIndexTrueBitNum - NetIndexFirstBitSegment) */ - int32 GetNetIndexFirstBitSegment() const { VerifyNetworkIndex(); return NetIndexFirstBitSegment; } - - /** This is the actual value for an invalid tag "None". This is computed at runtime as (Total number of tags) + 1 */ - FGameplayTagNetIndex GetInvalidTagNetIndex() const { VerifyNetworkIndex(); return InvalidTagNetIndex; } - - const TArray>& GetNetworkGameplayTagNodeIndex() const { VerifyNetworkIndex(); return NetworkGameplayTagNodeIndex; } - - DECLARE_TS_MULTICAST_DELEGATE_OneParam(FOnGameplayTagLoaded, const FGameplayTag& /*Tag*/) - FOnGameplayTagLoaded OnGameplayTagLoadedDelegate; - - /** Numbers of bits to use for replicating container size. This can be set via config. */ - int32 NumBitsForContainerSize; - - GAMEPLAYTAGS_API void PushDeferOnGameplayTagTreeChangedBroadcast(); - GAMEPLAYTAGS_API void PopDeferOnGameplayTagTreeChangedBroadcast(); - -private: - /** Cached number of bits we need to replicate tags. That is, Log2(Number of Tags). Will always be <= 16. */ - int32 NetIndexTrueBitNum; - - /** The length in bits of the first segment when net serializing tags. We will serialize NetIndexFirstBitSegment + 1 bit to indicatore "more" (more = second segment that is NetIndexTrueBitNum - NetIndexFirstBitSegment) */ - int32 NetIndexFirstBitSegment; - - /** This is the actual value for an invalid tag "None". This is computed at runtime as (Total number of tags) + 1 */ - FGameplayTagNetIndex InvalidTagNetIndex; - -public: - -#if WITH_EDITOR - /** Gets a Filtered copy of the GameplayRootTags Array based on the comma delimited filter string passed in */ - GAMEPLAYTAGS_API void GetFilteredGameplayRootTags(const FString& InFilterString, TArray< TSharedPtr >& OutTagArray) const; - - /** Returns "Categories" meta property from given handle, used for filtering by tag widget */ - GAMEPLAYTAGS_API FString GetCategoriesMetaFromPropertyHandle(TSharedPtr PropertyHandle) const; - - /** Helper function, made to be called by custom OnGetCategoriesMetaFromPropertyHandle handlers */ - static GAMEPLAYTAGS_API FString StaticGetCategoriesMetaFromPropertyHandle(TSharedPtr PropertyHandle); - - /** Returns "Categories" meta property from given field, used for filtering by tag widget */ - template - static FString GetCategoriesMetaFromField(TFieldType* Field) - { - check(Field); - if (Field->HasMetaData(NAME_Categories)) - { - return Field->GetMetaData(NAME_Categories); - } - else if (Field->HasMetaData(NAME_GameplayTagFilter)) - { - return Field->GetMetaData(NAME_GameplayTagFilter); - } - return FString(); - } - - /** Returns "GameplayTagFilter" meta property from given function, used for filtering by tag widget for any parameters of the function that end up as BP pins */ - static GAMEPLAYTAGS_API FString GetCategoriesMetaFromFunction(const UFunction* Func, FName ParamName = NAME_None); - - /** Gets a list of all gameplay tag nodes added by the specific source */ - GAMEPLAYTAGS_API void GetAllTagsFromSource(FName TagSource, TArray< TSharedPtr >& OutTagArray) const; - - /** Returns true if this tag was explicitly registered, this is false for implictly added parent tags */ - GAMEPLAYTAGS_API bool IsDictionaryTag(FName TagName) const; - - /** Returns information about tag. If not found return false */ - GAMEPLAYTAGS_API bool GetTagEditorData(FName TagName, FString& OutComment, FName &OutFirstTagSource, bool& bOutIsTagExplicit, bool &bOutIsRestrictedTag, bool &bOutAllowNonRestrictedChildren) const; - - /** Returns information about tag. If not found return false */ - GAMEPLAYTAGS_API bool GetTagEditorData(FName TagName, FString& OutComment, TArray& OutTagSources, bool& bOutIsTagExplicit, bool &bOutIsRestrictedTag, bool &bOutAllowNonRestrictedChildren) const; - - /** This is called after EditorRefreshGameplayTagTree. Useful if you need to do anything editor related when tags are added or removed */ - static GAMEPLAYTAGS_API FSimpleMulticastDelegate OnEditorRefreshGameplayTagTree; - - /** Refresh the gameplaytag tree due to an editor change */ - GAMEPLAYTAGS_API void EditorRefreshGameplayTagTree(); - - /** Suspends EditorRefreshGameplayTagTree requests */ - GAMEPLAYTAGS_API void SuspendEditorRefreshGameplayTagTree(FGuid SuspendToken); - - /** Resumes EditorRefreshGameplayTagTree requests; triggers a refresh if a request was made while it was suspended */ - GAMEPLAYTAGS_API void ResumeEditorRefreshGameplayTagTree(FGuid SuspendToken); - - /** Gets a Tag Container containing all of the tags in the hierarchy that are children of this tag, and were explicitly added to the dictionary */ - GAMEPLAYTAGS_API FGameplayTagContainer RequestGameplayTagChildrenInDictionary(const FGameplayTag& GameplayTag) const; -#if WITH_EDITORONLY_DATA - /** Gets a Tag Container containing all of the tags in the hierarchy that are children of this tag, were explicitly added to the dictionary, and do not have any explicitly added tags between them and the specified tag */ - GAMEPLAYTAGS_API FGameplayTagContainer RequestGameplayTagDirectDescendantsInDictionary(const FGameplayTag& GameplayTag, EGameplayTagSelectionType SelectionType) const; -#endif // WITH_EDITORONLY_DATA - - - DECLARE_MULTICAST_DELEGATE_TwoParams(FOnGameplayTagDoubleClickedEditor, FGameplayTag, FSimpleMulticastDelegate& /* OUT */) - FOnGameplayTagDoubleClickedEditor OnGatherGameplayTagDoubleClickedEditor; - - /** Chance to dynamically change filter string based on a property handle */ - DECLARE_MULTICAST_DELEGATE_TwoParams(FOnGetCategoriesMetaFromPropertyHandle, TSharedPtr, FString& /* OUT */) - FOnGetCategoriesMetaFromPropertyHandle OnGetCategoriesMetaFromPropertyHandle; - - /** Allows dynamic hiding of gameplay tags in SGameplayTagWidget. Allows higher order structs to dynamically change which tags are visible based on its own data */ - DECLARE_MULTICAST_DELEGATE_ThreeParams(FOnFilterGameplayTagChildren, const FString& /** FilterString */, TSharedPtr& /* TagNode */, bool& /* OUT OutShouldHide */) - FOnFilterGameplayTagChildren OnFilterGameplayTagChildren; - - /* - * This is a container to filter out gameplay tags when they are invalid or when they don't meet the filter string - * If used from editor to filter out tags when picking them the FilterString is optional and the ReferencingPropertyHandle is required - * If used to validate an asset / assets you can provide the TagSourceAssets. The FilterString and ReferencingPropertyHandle is optional - */ - struct FFilterGameplayTagContext - { - const FString& FilterString; - const TSharedPtr& TagNode; - const FGameplayTagSource* TagSource; - const TSharedPtr ReferencingPropertyHandle; - const TArray TagSourceAssets; - - FFilterGameplayTagContext(const FString& InFilterString, const TSharedPtr& InTagNode, const FGameplayTagSource* InTagSource, const TSharedPtr& InReferencingPropertyHandle) - : FilterString(InFilterString), TagNode(InTagNode), TagSource(InTagSource), ReferencingPropertyHandle(InReferencingPropertyHandle) - {} - - FFilterGameplayTagContext(const TSharedPtr& InTagNode, const FGameplayTagSource* InTagSource, const TArray& InTagSourceAssets, const FString& InFilterString = FString()) - : FilterString(InFilterString), TagNode(InTagNode), TagSource(InTagSource), TagSourceAssets(InTagSourceAssets) - {} - }; - - /* - * Allows dynamic hiding of gameplay tags in SGameplayTagWidget. Allows higher order structs to dynamically change which tags are visible based on its own data - * Applies to all tags, and has more context than OnFilterGameplayTagChildren - */ - DECLARE_MULTICAST_DELEGATE_TwoParams(FOnFilterGameplayTag, const FFilterGameplayTagContext& /** InContext */, bool& /* OUT OutShouldHide */) - FOnFilterGameplayTag OnFilterGameplayTag; - - GAMEPLAYTAGS_API void NotifyGameplayTagDoubleClickedEditor(FString TagName); - - GAMEPLAYTAGS_API bool ShowGameplayTagAsHyperLinkEditor(FString TagName); - - /** - * Used for incremental cooking. Create an FCookDependency that reports tags that have been read from ini. - * Packages that pass this dependency to AddCookLoadDependency or AddCookSaveDependency in their OnCookEvent or - * (if Ar.IsCooking()) Serialize function will be invalidated and recooked by the incremental cook whenever those - * tags change. - */ - GAMEPLAYTAGS_API UE::Cook::FCookDependency CreateCookDependency(); - - /** Implementation of console command GameplayTags.DumpSources */ - void DumpSources(FOutputDevice& Out) const; -#endif //WITH_EDITOR - - GAMEPLAYTAGS_API void PrintReplicationIndices(); - int32 GetNumGameplayTagNodes() const { return GameplayTagNodeMap.Num(); } - -#if !(UE_BUILD_SHIPPING || UE_BUILD_TEST) - /** Mechanism for tracking what tags are frequently replicated */ - - GAMEPLAYTAGS_API void PrintReplicationFrequencyReport(); - GAMEPLAYTAGS_API void NotifyTagReplicated(FGameplayTag Tag, bool WasInContainer); - - TMap ReplicationCountMap; - TMap ReplicationCountMap_SingleTags; - TMap ReplicationCountMap_Containers; -#endif - -private: - - /** Initializes the manager */ - static GAMEPLAYTAGS_API void InitializeManager(); - - /** finished loading/adding native tags */ - static GAMEPLAYTAGS_API FSimpleMulticastDelegate& OnDoneAddingNativeTagsDelegate(); - - /** The Tag Manager singleton */ - static GAMEPLAYTAGS_API UGameplayTagsManager* SingletonManager; - - friend class FGameplayTagTest; - friend class FGameplayEffectsTest; - friend class FGameplayTagsModule; - friend class FGameplayTagsEditorModule; - friend class UGameplayTagsSettings; - friend class SAddNewGameplayTagSourceWidget; - friend class FNativeGameplayTag; - - /** - * Helper function to get the stored TagContainer containing only this tag, which has searchable ParentTags - * NOTE: This function is not threadsafe and should only be used in code that locks the tag map critical section - * @param GameplayTag Tag to get single container of - * @return Pointer to container with this tag - */ - inline const FGameplayTagContainer* GetSingleTagContainerPtr(const FGameplayTag& GameplayTag) const - { - // Doing this with pointers to avoid a shared ptr reference count change - const TSharedPtr* Node = GameplayTagNodeMap.Find(GameplayTag); - - if (Node) - { - return &(*Node)->GetSingleTagContainer(); - } -#if WITH_EDITOR - // Check redirector - if (GIsEditor && GameplayTag.IsValid()) - { - FGameplayTag RedirectedTag = GameplayTag; - - RedirectSingleGameplayTag(RedirectedTag, nullptr); - - Node = GameplayTagNodeMap.Find(RedirectedTag); - - if (Node) - { - return &(*Node)->GetSingleTagContainer(); - } - } -#endif - return nullptr; - } - - - /** - * Helper function to insert a tag into a tag node array - * - * @param Tag Short name of tag to insert - * @param FullTag Full tag, passed in for performance - * @param ParentNode Parent node, if any, for the tag - * @param NodeArray Node array to insert the new node into, if necessary (if the tag already exists, no insertion will occur) - * @param SourceName File tag was added from - * @param DevComment Comment from developer about this tag - * @param bIsExplicitTag Is the tag explicitly defined or is it implied by the existence of a child tag - * @param bIsRestrictedTag Is the tag a restricted tag or a regular gameplay tag - * @param bAllowNonRestrictedChildren If the tag is a restricted tag, can it have regular gameplay tag children or should all of its children be restricted tags as well? - * - * @return Index of the node of the tag - */ - GAMEPLAYTAGS_API int32 InsertTagIntoNodeArray(FName Tag, FName FullTag, TSharedPtr ParentNode, TArray< TSharedPtr >& NodeArray, FName SourceName, const FString& DevComment, bool bIsExplicitTag, bool bIsRestrictedTag, bool bAllowNonRestrictedChildren); - - /** Helper function to populate the tag tree from each table */ - GAMEPLAYTAGS_API void PopulateTreeFromDataTable(class UDataTable* Table); - - GAMEPLAYTAGS_API void AddTagTableRow(const FGameplayTagTableRow& TagRow, FName SourceName, bool bIsRestrictedTag = false); - - GAMEPLAYTAGS_API void AddChildrenTags(FGameplayTagContainer& TagContainer, TSharedPtr GameplayTagNode, bool RecurseAll=true, bool OnlyIncludeDictionaryTags=false) const; - - GAMEPLAYTAGS_API void AddRestrictedGameplayTagSource(const FString& FileName); - - GAMEPLAYTAGS_API void AddTagsFromAdditionalLooseIniFiles(const TArray& IniFileList); - - /** - * Helper function for GameplayTagsMatch to get all parents when doing a parent match, - * NOTE: Must never be made public as it uses the FNames which should never be exposed - * - * @param NameList The list we are adding all parent complete names too - * @param GameplayTag The current Tag we are adding to the list - */ - GAMEPLAYTAGS_API void GetAllParentNodeNames(TSet& NamesList, TSharedPtr GameplayTag) const; - - /** Returns the tag source for a given tag source name, or null if not found */ - GAMEPLAYTAGS_API FGameplayTagSource* FindOrAddTagSource(FName TagSourceName, EGameplayTagSourceType SourceType, const FString& RootDirToUse = FString()); - - /** Constructs the net indices for each tag */ - GAMEPLAYTAGS_API void ConstructNetIndex(); - - /** Marks all of the nodes that descend from CurNode as having an ancestor node that has a source conflict. */ - GAMEPLAYTAGS_API void MarkChildrenOfNodeConflict(TSharedPtr CurNode); - - void VerifyNetworkIndex() const - { - if (!bUseFastReplication) - { - UE_LOG(LogGameplayTags, Warning, TEXT("%hs called when not using FastReplication (not rebuilding the fast replication cache)"), __func__); - } - else if (bNetworkIndexInvalidated) - { - const_cast(this)->ConstructNetIndex(); - } - } - - void InvalidateNetworkIndex() { bNetworkIndexInvalidated = true; } - - /** Called in both editor and game when the tag tree changes during startup or editing */ - GAMEPLAYTAGS_API void BroadcastOnGameplayTagTreeChanged(); - - /** Call after modifying the tag tree nodes, this will either call the full editor refresh or a limited game refresh */ - GAMEPLAYTAGS_API void HandleGameplayTagTreeChanged(bool bRecreateTree); - -#if WITH_EDITOR - void UpdateIncrementalCookHash(UE::Cook::ICookInfo& CookInfo); -#endif - - // Tag Sources - /////////////////////////////////////////////////////// - - /** These are the old native tags that use to be resisted via a function call with no specific site/ownership. */ - TSet LegacyNativeTags; - - /** Map of all config directories to load tag inis from */ - TMap RegisteredSearchPaths; - - /** Roots of gameplay tag nodes */ - TSharedPtr GameplayRootTag; - - /** Map of Tags to Nodes - Internal use only. FGameplayTag is inside node structure, do not use FindKey! */ - TMap> GameplayTagNodeMap; - - /** Our aggregated, sorted list of commonly replicated tags. These tags are given lower indices to ensure they replicate in the first bit segment. */ - TArray CommonlyReplicatedTags; - - /** Map of gameplay tag source names to source objects */ - UPROPERTY() - TMap TagSources; - - TSet RestrictedGameplayTagSourceNames; - - bool bIsConstructingGameplayTagTree = false; - - /** Cached runtime value for whether we are using fast replication or not. Initialized from config setting. */ - bool bUseFastReplication; - - /** Cached runtime value for whether we are using dynamic replication or not. Initialized from the config setting. */ - bool bUseDynamicReplication; - - /** Cached runtime value for whether we should warn when loading invalid tags */ - bool bShouldWarnOnInvalidTags; - - /** Cached runtime value for whether we should allow unloading of tags */ - bool bShouldAllowUnloadingTags; - - /** Augments usage of bShouldAllowUnloadingTags to allow runtime overrides to allow/disallow unloading of GameplayTags in controlled scenarios */ - TOptional ShouldAllowUnloadingTagsOverride; - - /** Used to suppress calls to HandleGameplayTagTreeChanged that would result in a complete rebuild of the GameplayTag tree*/ - TOptional ShouldDeferGameplayTagTreeRebuilds; - - /** True if native tags have all been added and flushed */ - bool bDoneAddingNativeTags; - - int32 bDeferBroadcastOnGameplayTagTreeChanged = 0; - bool bShouldBroadcastDeferredOnGameplayTagTreeChanged = false; - - /** If true, an action that would require a tree rebuild was performed during initialization **/ - bool bNeedsTreeRebuildOnDoneAddingGameplayTags = false; - - /** String with outlawed characters inside tags */ - FString InvalidTagCharacters; - - // This critical section is to handle an issue where tag requests come from another thread when async loading from a background thread in FGameplayTagContainer::Serialize. - // This class is not generically threadsafe, but this should be locked by any operation that could update something read by a background thread. - mutable FTransactionallySafeCriticalSection GameplayTagMapCritical; - -#if WITH_EDITOR - // Transient editor-only tags to support quick-iteration PIE workflows - TSet TransientEditorTags; - - TSet EditorRefreshGameplayTagTreeSuspendTokens; - bool bEditorRefreshGameplayTagTreeRequestedDuringSuspend = false; - - FBlake3Hash IncrementalCookHash; -#endif //if WITH_EDITOR - - /** Sorted list of nodes, used for network replication */ - TArray> NetworkGameplayTagNodeIndex; - - uint32 NetworkGameplayTagNodeIndexHash; - - bool bNetworkIndexInvalidated = true; - - /** Holds all of the valid gameplay-related tags that can be applied to assets */ - UPROPERTY() - TArray> GameplayTagTables; - - GAMEPLAYTAGS_API const static FName NAME_Categories; - GAMEPLAYTAGS_API const static FName NAME_GameplayTagFilter; - - friend class UGameplayTagsManagerIncrementalCookFunctions; -}; diff --git a/samples/GeomUtils.h b/samples/GeomUtils.h deleted file mode 100644 index e661ad5..0000000 --- a/samples/GeomUtils.h +++ /dev/null @@ -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 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 \ No newline at end of file diff --git a/ue_mcp_server.py b/ue_mcp_server.py new file mode 100644 index 0000000..881ac45 --- /dev/null +++ b/ue_mcp_server.py @@ -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()