# ML_GameUtilities — Blueprint Macro Library (100% BP, No C++)

> **This file is the pure-Blueprint equivalent of `FL_GameUtilities`.**
> Every function from the C++ Function Library is re-implemented as a Blueprint Macro Library macro using only engine-provided Blueprint nodes. No C++ compilation required.
>
> **C++ counterpart:** [`02_FL_GameUtilities.md`](02_FL_GameUtilities.md) | [`FL_GameUtilities.h`](../../Source/PG_Framework/Public/Core/FL_GameUtilities.h)

## Asset Details

| Property | Value |
|----------|-------|
| **Asset Type** | `Macro Library` |
| **Name** | `ML_GameUtilities` |
| **Path** | `Content/Framework/Core/ML_GameUtilities` |
| **Categorization** | Framework\|Utilities |

## Purpose

A collection of reusable Blueprint Macro nodes providing common utility functions for the entire project. All macros use only built-in Unreal Engine Blueprint nodes — **zero C++ required**. Any Blueprint, component, widget, or AI asset can call these macros by searching the context menu.

## Macro Library vs Function Library

| Aspect | FL_GameUtilities (C++) | ML_GameUtilities (Pure BP) |
|--------|------------------------|----------------------------|
| **Requires C++?** | Yes — BlueprintFunctionLibrary must be authored in C++ | No — Macro Library is a native UE5 asset type |
| **Can be pure (no exec pin)?** | Yes — `BlueprintPure` functions return inline | Macros can be pure, but complex logic needs exec pins |
| **Multi-output pins?** | Only with `UFUNCTION` output params | Yes — any number of output pins |
| **World context support?** | Via `meta = (WorldContext = "...")` | Via `WorldContext` pin on the macro |
| **Called from Construction Script?** | Pure functions: yes | Pure macros: yes. Impure macros: no (construction scripts reject exec pins) |
| **Where to find in context menu?** | Under `Framework\|Utilities\|...` | Under `Framework\|Utilities\|...` (same categories) |

## Macro Categories

| Category | Macros | Pure / Impure |
|----------|--------|---------------|
| **Subsystem Access** | `GetSubsystemSafe`, `GetGameFramework`, `GetPlayerController`, `GetSubsystemByClass` | Impure (need exec pins for null-check branching) |
| **Actor Utilities** | `FindComponentByInterface`, `FindNearestActorWithTag` | Impure |
| **Math** | `RemapFloat`, `LerpClamped`, `VectorToAngle2D`, `AngleDifference` | Pure (no exec pins) |
| **Gameplay Tags** | `HasGameplayTag`, `AddGameplayTagToActor`, `RemoveGameplayTagFromActor`, `MakeTagFromString`, `TagContainerHasAny` | Mixed (queries pure, mutations impure) |
| **Text Formatting** | `FormatTime`, `Pluralise`, `OrdinalSuffix`, `TruncateText` | Pure |
| **Screen / Viewport** | `WorldToScreenSafe`, `ClampToViewport` | Impure |
| **Debug** | `LogDebug`, `DrawDebugSphere`, `DrawDebugString3D` | Impure |

---

## 1. Enums

No enums are defined in this library.

---

## 2. Structs

No structs are defined in this library. All inputs/outputs use native Blueprint types.

---

## 3. Variables

This is a Macro Library. It has **no variables, no event dispatchers, no tick**. All macros are self-contained — they only use their input pins and output their results.

---

## 4. Macros (Function Definitions)

---

### 4.1 Subsystem Access Macros

#### Macro: `GetSubsystemSafe`
- **Category:** `Framework|Utilities|Subsystems`
- **Description:** Null-safe subsystem retrieval. Returns `None` instead of crashing if the GameInstance or subsystem doesn't exist.
- **Input Pins:**
  | Name | Type | Description |
  |------|------|-------------|
  | `exec` | Exec | Execution input (impure) |
  | `WorldContextObject` | Object | World context for finding the GameInstance |
  | `SubsystemClass` | Class (GameInstanceSubsystem) | The subsystem class to retrieve |
- **Output Pins:**
  | Name | Type | Description |
  |------|------|-------------|
  | `Subsystem` | GameInstanceSubsystem | The found subsystem, or None if not found |
- **Node-by-Node Logic:**
  ```
  [Macro Body: GetSubsystemSafe]
    Inputs: WorldContextObject (Object), SubsystemClass (Class)
    
    Step 1: Call "Get Game Instance" (WorldContextObject) → GameInstance (GameInstance)
    Step 2: Branch on "Is Valid" (GameInstance)
      True →
        Step 2a: Call "Get Subsystem (Class)" on GameInstance, passing SubsystemClass → Subsystem (GameInstanceSubsystem)
        Step 2b: Return Subsystem
      False →
        Step 2c: Return None
  ```
- **Nodes to Search For:**
  - `Get Game Instance`
  - `Is Valid` (Object)
  - `Branch`
  - `Get Subsystem (Class)`
- **Macro Graph Setup:**
  1. Add `WorldContextObject` input pin (type: `Object`).
  2. Add `SubsystemClass` input pin (type: `Class Reference` → `GameInstanceSubsystem`).
  3. Add `Subsystem` output pin (type: `GameInstanceSubsystem`).
  4. Wire exec from input → `Get Game Instance` → `Branch` on validity → true branch calls `Get Subsystem (Class)` and returns.

---

#### Macro: `GetGameFramework`
- **Category:** `Framework|Utilities|Subsystems`
- **Description:** Typed retrieval of `GI_GameFramework` Game Instance. Returns `None` if cast fails or GameInstance doesn't exist.
- **Input Pins:**
  | Name | Type | Description |
  |------|------|-------------|
  | `exec` | Exec | Execution input |
  | `WorldContextObject` | Object | World context |
- **Output Pins:**
  | Name | Type | Description |
  |------|------|-------------|
  | `GameFramework` | GI_GameFramework | The Game Framework instance, or None |
- **Node-by-Node Logic:**
  ```
  [Macro Body: GetGameFramework]
    Step 1: Call "Get Game Instance" (WorldContextObject) → GameInstance (GameInstance)
    Step 2: Branch on "Is Valid" (GameInstance)
      True →
        Step 2a: "Cast to GI_GameFramework" (GameInstance) → CastResult (GI_GameFramework)
        Step 2b: Branch on "Is Valid" (CastResult)
          True → Return CastResult
          False → Print String Warning: "ML_GameUtilities: GetGameFramework failed — wrong GameInstance type" → Return None
      False →
        Step 2c: Print String Warning: "ML_GameUtilities: No GameInstance found" → Return None
  ```
- **Nodes to Search For:**
  - `Get Game Instance`
  - `Is Valid`
  - `Branch`
  - `Cast to GI_GameFramework`
  - `Print String`
- **Note:** Replace `GI_GameFramework` with your actual Game Instance class reference. If your Game Instance has a different name, cast to that instead.

---

#### Macro: `GetPlayerController`
- **Category:** `Framework|Utilities|Subsystems`
- **Description:** Typed retrieval of `PC_CoreController` for player index 0. Returns `None` if cast fails or controller doesn't exist.
- **Input Pins:**
  | Name | Type | Description |
  |------|------|-------------|
  | `exec` | Exec | Execution input |
  | `WorldContextObject` | Object | World context |
- **Output Pins:**
  | Name | Type | Description |
  |------|------|-------------|
  | `PlayerController` | PC_CoreController | The player controller, or None |
- **Node-by-Node Logic:**
  ```
  [Macro Body: GetPlayerController]
    Step 1: Call "Get Player Controller" (Player Index 0, WorldContextObject) → PC (PlayerController)
    Step 2: Branch on "Is Valid" (PC)
      True →
        Step 2a: "Cast to PC_CoreController" (PC) → CoreController (PC_CoreController)
        Step 2b: Return CoreController
      False →
        Step 2c: Return None
  ```
- **Nodes to Search For:**
  - `Get Player Controller`
  - `Is Valid`
  - `Branch`
  - `Cast to PC_CoreController`

---

#### Macro: `GetSubsystemByClass`
- **Category:** `Framework|Utilities|Subsystems`
- **Description:** Generic subsystem retrieval by class. Returns the subsystem cast to the specified class. Returns `None` if not found or cast fails.
- **Input Pins:**
  | Name | Type | Description |
  |------|------|-------------|
  | `exec` | Exec | Execution input |
  | `WorldContextObject` | Object | World context |
  | `SubsystemClass` | Class (GameInstanceSubsystem) | The subsystem class to retrieve |
- **Output Pins:**
  | Name | Type | Description |
  |------|------|-------------|
  | `Subsystem` | GameInstanceSubsystem | The found subsystem, or None |
- **Node-by-Node Logic:**
  ```
  [Macro Body: GetSubsystemByClass]
    Step 1: Call "GetSubsystemSafe" macro (WorldContextObject, SubsystemClass) → Subsystem
    Step 2: Return Subsystem
  ```
- **Nodes to Search For:**
  - `GetSubsystemSafe` (internal macro reuse — drag in the macro from this same library)
- **Note:** This is a thin wrapper that delegates to `GetSubsystemSafe`. Since Blueprint macros can call other macros, you can simply insert the `GetSubsystemSafe` macro directly inside this one. This avoids duplicating the null-safety logic.

---

### 4.2 Actor Utility Macros

#### Macro: `FindComponentByInterface`
- **Category:** `Framework|Utilities|Actor`
- **Description:** Finds the first component on an actor that implements a specified interface.
- **Input Pins:**
  | Name | Type | Description |
  |------|------|-------------|
  | `exec` | Exec | Execution input |
  | `TargetActor` | Actor | Actor to search |
  | `InterfaceClass` | Interface | The interface to check for |
- **Output Pins:**
  | Name | Type | Description |
  |------|------|-------------|
  | `FoundComponent` | ActorComponent | The first matching component, or None |
  | `bFound` | Boolean | True if a component was found |
- **Node-by-Node Logic:**
  ```
  [Macro Body: FindComponentByInterface]
    Step 1: Branch on "Is Valid" (TargetActor)
      False → Return None, bFound=False
    
    Step 2: Call "Get Components by Class" (TargetActor, class: ActorComponent) → Components (Array<ActorComponent>)
    
    Step 3: For Each Loop (Components)
      Loop Body:
        Step 3a: Call "Does Implement Interface" (Array Element, InterfaceClass) → bImplements (Bool)
        Step 3b: Branch on bImplements
          True → Set FoundComponent = Array Element, Set bFound = True, break the loop (use "For Loop with Break" instead)
          False → Continue
    
    Step 4: Return FoundComponent, bFound
  ```
- **Nodes to Search For:**
  - `Is Valid`
  - `Branch`
  - `Get Components by Class`
  - `For Loop with Break` (right-click → "For Loop with Break")
  - `Does Implement Interface`
- **Things to Watch:**
  - Use `For Loop with Break` instead of `For Each Loop` — you need to stop when you find the first match.
  - Cast the array element to `ActorComponent` before passing to `Does Implement Interface` — UE5 may need the explicit cast.
  - `Get Components by Class` returns components from Blueprint-defined components AND runtime-added components.

---

#### Macro: `FindNearestActorWithTag`
- **Category:** `Framework|Utilities|Actor`
- **Description:** Finds the nearest actor within a radius that has a specific gameplay tag. Returns `None` if no matching actor found.
- **Input Pins:**
  | Name | Type | Description |
  |------|------|-------------|
  | `exec` | Exec | Execution input |
  | `Origin` | Vector | World position to search from |
  | `Radius` | Float | Search radius in world units |
  | `RequiredTag` | GameplayTag (Struct) | The tag an actor must have |
  | `ActorsToIgnore` | Array of Actor | Actors to exclude from search |
- **Output Pins:**
  | Name | Type | Description |
  |------|------|-------------|
  | `NearestActor` | Actor | The closest matching actor, or None |
  | `Distance` | Float | Distance to the nearest actor, or -1 if none found |
- **Node-by-Node Logic:**
  ```
  [Macro Body: FindNearestActorWithTag]
    Step 1: Set local vars: NearestActor = None, NearestDist = -1 (or Max Float)
    
    Step 2: Call "Get All Actors with Tag" (RequiredTag.TagName as Name) → TaggedActors (Array<Actor>)
      ⚠️ "Get All Actors with Tag" takes an FName, not an FGameplayTag.
      Workaround: Use "Break GameplayTag" to get TagName, or use "Get All Actors of Class with Tag" if available.
      Alternative (more robust): Use "Get All Actors of Class" (Actor) → For Loop → check "Does Actor Have Tag" on each.
    
    Step 3: For Each Loop (TaggedActors)
      Array Element → Branch on "Is Valid"
        True →
          Step 3a: Check if Element is in "ActorsToIgnore" (use "Array Contains Item") → Branch
          True (skip) → Continue
          False →
            Step 3b: Call "Get Distance To" (Element, Origin) → Dist (Float)
            Step 3c: Branch: Dist <= Radius
              True → Branch: Dist < NearestDist OR NearestDist == -1
                True → Set NearestActor = Element, NearestDist = Dist
                False → Continue
              False → Continue
    
    Step 4: Branch on "Is Valid" (NearestActor)
      True → Return NearestActor, Distance = NearestDist
      False → Return None, Distance = -1
  ```
- **Nodes to Search For:**
  - `Get All Actors of Class with Tag` (if your engine version supports it with GameplayTags)
  - **Better alternative:** `Get All Actors of Class` (Actor) + `Does Actor Have Tag` (for each)
  - Or: `Sphere Overlap Actors` (more performant for radius-based search)
  - `For Each Loop`
  - `Get Distance To` (Vector)
  - `Array Contains Item`
  - `Branch`
  - `Break GameplayTag` (to get TagName)
- **Performance Note:** For large levels, `Get All Actors of Class` is expensive. Consider using `Sphere Overlap Actors` with the Origin and Radius as the sphere — it's faster and respects collision channels.

**Optimized Alternative Flow (using Sphere Overlap):**
  ```
  Step 1: Create a temporary "Sphere Collision" trace → use "Sphere Overlap Actors" (Origin, Radius) → OverlappedActors
  Step 2: For each overlapped actor → call "Does Actor Have Tag" (RequiredTag) → filter
  Step 3: For each tagged actor → "Get Distance To" → track nearest
  ```

---

### 4.3 Math Macros (All Pure)

#### Macro: `RemapFloat`
- **Category:** `Framework|Utilities|Math`
- **Description:** Remaps a value from [InMin, InMax] to [OutMin, OutMax]. Clamped to output range.
- **Input Pins:**
  | Name | Type | Description |
  |------|------|-------------|
  | `Value` | Float | Value to remap |
  | `InMin` | Float | Input range minimum |
  | `InMax` | Float | Input range maximum |
  | `OutMin` | Float | Output range minimum |
  | `OutMax` | Float | Output range maximum |
- **Output Pins:**
  | Name | Type | Description |
  |------|------|-------------|
  | `Result` | Float | Remapped value |
- **Node-by-Node Logic:**
  ```
  [Macro Body: RemapFloat] (PURE — check "Pure" in macro settings)
    Step 1: Call "Map Range Clamped" (Value, InRangeA=InMin, InRangeB=InMax, OutRangeA=OutMin, OutRangeB=OutMax) → Result
    Step 2: Return Result
  ```
- **Nodes to Search For:**
  - `Map Range Clamped` (built-in math node — "Map Range" then set Clamped to true)
- **One-Liner:** This macro is a direct wrapper around `Map Range Clamped`. You can even skip this macro and use `Map Range Clamped` directly.

---

#### Macro: `LerpClamped`
- **Category:** `Framework|Utilities|Math`
- **Description:** Linear interpolation with alpha clamped to [0, 1].
- **Input Pins:**
  | Name | Type | Description |
  |------|------|-------------|
  | `A` | Float | Start value |
  | `B` | Float | End value |
  | `Alpha` | Float | Interpolation factor (clamped 0-1) |
- **Output Pins:**
  | Name | Type | Description |
  |------|------|-------------|
  | `Result` | Float | Interpolated value |
- **Node-by-Node Logic:**
  ```
  [Macro Body: LerpClamped] (PURE)
    Step 1: Call "Clamp (Float)" (Alpha, Min=0.0, Max=1.0) → ClampedAlpha (Float)
    Step 2: Call "Lerp (Float)" (A, B, ClampedAlpha) → Result
    Step 3: Return Result
  ```
- **Nodes to Search For:**
  - `Clamp (Float)`
  - `Lerp (Float)`

---

#### Macro: `VectorToAngle2D`
- **Category:** `Framework|Utilities|Math`
- **Description:** Converts a 2D direction vector (X, Y) to an angle in degrees (0-360), where 0° = +X, 90° = +Y.
- **Input Pins:**
  | Name | Type | Description |
  |------|------|-------------|
  | `Direction` | Vector2D | 2D direction vector |
- **Output Pins:**
  | Name | Type | Description |
  |------|------|-------------|
  | `Angle` | Float | Angle in degrees (0-360) |
- **Node-by-Node Logic:**
  ```
  [Macro Body: VectorToAngle2D] (PURE)
    Step 1: Call "Deg Atan2" (Y=Direction.Y, X=Direction.X) → RawAngle (Float, degrees, range -180 to 180)
    Step 2: Call "Select Float" (Condition = RawAngle < 0, True=RawAngle + 360, False=RawAngle) → NormalizedAngle
    Step 3: Return NormalizedAngle
  ```
- **Nodes to Search For:**
  - `Deg Atan2` (or `Atan2 (degrees)`)
  - `Select Float`
  - `Float + Float`
- **If `DegAtan2` doesn't exist:** Use `Atan2 (Radians)` → `Radians to Degrees` → then normalize.

---

#### Macro: `AngleDifference`
- **Category:** `Framework|Utilities|Math`
- **Description:** Returns the shortest signed angle difference between two angles in degrees. Result is in range [-180, 180].
- **Input Pins:**
  | Name | Type | Description |
  |------|------|-------------|
  | `AngleA` | Float | First angle in degrees |
  | `AngleB` | Float | Second angle in degrees |
- **Output Pins:**
  | Name | Type | Description |
  |------|------|-------------|
  | `Difference` | Float | Signed difference in degrees [-180, 180] |
- **Node-by-Node Logic:**
  ```
  [Macro Body: AngleDifference] (PURE)
    Step 1: Float subtraction: Diff = AngleB - AngleA
    
    Step 2: Call "FMod" (Diff, 360.0) → WrappedDiff (Float)
      (or use: Diff - Floor(Diff / 360) * 360)
    
    Step 3: Branch: WrappedDiff > 180
      True → Return WrappedDiff - 360
      False →
        Branch: WrappedDiff < -180
          True → Return WrappedDiff + 360
          False → Return WrappedDiff
  ```
- **Nodes to Search For:**
  - `Float - Float`
  - `FMod` (Float modulo) — or use `%` modulo node (search "modulo" or "%")
  - `Select Float` (for the conditional logic)
- **Simpler Alternative:**
  If your engine has `Delta (Rotator)` or similar: Convert both angles to rotators → `Delta (Rotator)` → extract Yaw → return.
  However, the FMod approach shown above is the most reliable.

---

### 4.4 Gameplay Tag Macros

#### Macro: `HasGameplayTag`
- **Category:** `Framework|Utilities|Tags`
- **Description:** Safe check if an actor's gameplay tag container includes the specified tag. Returns `False` if actor is invalid.
- **Input Pins:**
  | Name | Type | Description |
  |------|------|-------------|
  | `TargetActor` | Actor | The actor to check |
  | `Tag` | GameplayTag (Struct) | The tag to look for |
- **Output Pins:**
  | Name | Type | Description |
  |------|------|-------------|
  | `bHasTag` | Boolean | True if the actor has the tag |
- **Node-by-Node Logic:**
  ```
  [Macro Body: HasGameplayTag] (PURE)
    Step 1: Branch on "Is Valid" (TargetActor)
      False → Return False
    
    Step 2: Call "Does Actor Have Tag" (TargetActor, Tag) → bHasTag (Bool)
      (search in context menu: "Does Actor Have Tag" or "Actor Has Tag")
    Step 3: Return bHasTag
  ```
- **Nodes to Search For:**
  - `Does Actor Have Tag`
  - `Is Valid`
  - `Select Bool` (for the invalid-actor → false path); or in pure mode use a `Branch` setup... 
  
  **⚠️ Pure Mode Branching Issue:** Pure macros can't use `Branch` nodes. Use this workaround:
  ```
  Step 1: "Does Actor Have Tag" (TargetActor, Tag) → bResult
  Step 2: "And" logic: "Is Valid" (TargetActor) AND bResult → Return
  ```
  This works because `Does Actor Have Tag` on an invalid actor simply returns `False` in UE5.
  
  **Simpler:** Just call `Does Actor Have Tag` directly — it's already null-safe. The macro is just a convenience wrapper.

---

#### Macro: `AddGameplayTagToActor`
- **Category:** `Framework|Utilities|Tags`
- **Description:** Adds a gameplay tag to an actor's tag container. Logs a warning if the actor doesn't have a tag container.
- **Input Pins:**
  | Name | Type | Description |
  |------|------|-------------|
  | `exec` | Exec | Execution input |
  | `TargetActor` | Actor | The actor to modify |
  | `Tag` | GameplayTag (Struct) | The tag to add |
- **Output Pins:**
  | Name | Type | Description |
  |------|------|-------------|
  | `exec` | Exec | Execution output |
- **Node-by-Node Logic:**
  ```
  [Macro Body: AddGameplayTagToActor]
    Step 1: Branch on "Is Valid" (TargetActor)
      False → Print String Warning: "AddGameplayTagToActor: TargetActor is None" → Return
    
    Step 2: Call "Get Actor Gameplay Tag Container" (TargetActor) → TagContainer (GameplayTagContainer)
      (search: "Get Actor Gameplay Tag Container")
    
    Step 3: Branch on "Is Valid" (TagContainer reference — actually UE5 returns by ref, so skip this check)
      → Call "Add Gameplay Tag" (TagContainer, Tag)
      → Return
  ```
- **Nodes to Search For:**
  - `Get Actor Gameplay Tag Container`
  - `Add Gameplay Tag` (GameplayTagContainer function)
  - `Is Valid`
  - `Branch`
  - `Print String`
- **Alternative:** Use the `I_GameplayTagAssetInterface` if your actors implement it. Call `Add Gameplay Tag` on the interface reference.

---

#### Macro: `RemoveGameplayTagFromActor`
- **Category:** `Framework|Utilities|Tags`
- **Description:** Removes a gameplay tag from an actor's tag container. No-op if actor is invalid.
- **Input Pins:**
  | Name | Type | Description |
  |------|------|-------------|
  | `exec` | Exec | Execution input |
  | `TargetActor` | Actor | The actor to modify |
  | `Tag` | GameplayTag (Struct) | The tag to remove |
- **Output Pins:**
  | Name | Type | Description |
  |------|------|-------------|
  | `exec` | Exec | Execution output |
- **Node-by-Node Logic:**
  ```
  [Macro Body: RemoveGameplayTagFromActor]
    Step 1: Branch on "Is Valid" (TargetActor)
      False → Return
    
    Step 2: Call "Get Actor Gameplay Tag Container" (TargetActor) → TagContainer
    Step 3: Call "Remove Gameplay Tag" (TagContainer, Tag)
    Step 4: Return
  ```
- **Nodes to Search For:**
  - `Get Actor Gameplay Tag Container`
  - `Remove Gameplay Tag`

---

#### Macro: `MakeTagFromString`
- **Category:** `Framework|Utilities|Tags`
- **Description:** Creates a gameplay tag from a string. Returns `EmptyTag` if the tag is not registered in the tag table.
- **Input Pins:**
  | Name | Type | Description |
  |------|------|-------------|
  | `TagString` | String or Name | The tag string (e.g., "State.Action.Reloading") |
- **Output Pins:**
  | Name | Type | Description |
  |------|------|-------------|
  | `Tag` | GameplayTag (Struct) | The resulting gameplay tag |
- **Node-by-Node Logic:**
  ```
  [Macro Body: MakeTagFromString] (PURE)
    Step 1: Call "Make Literal GameplayTag" (Value=TagString) → Tag
      (search: "Make Literal GameplayTag")
    Step 2: Return Tag
  ```
- **Nodes to Search For:**
  - `Make Literal GameplayTag`
- **Note:** `Make Literal GameplayTag` returns an empty tag if the string doesn't match any registered tag. This is identical behavior to the `RequestGameplayTag` C++ function.
- **Alternative:** If you have a `Name` instead of a `String`, use `Make GameplayTag from Name`. Convert String → Name first if needed: `String to Name`.
- **⚠️ Gotcha:** Your GameplayTags must be registered in the project's `DefaultGameplayTags.ini` or an `IniSettings` DataTable. If the tag isn't registered, the result is silently empty. Always validate critical tags at startup.

---

#### Macro: `TagContainerHasAny`
- **Category:** `Framework|Utilities|Tags`
- **Description:** Returns `True` if the container has any of the tags in the `TagsToCheck` container. This is an OR query.
- **Input Pins:**
  | Name | Type | Description |
  |------|------|-------------|
  | `Container` | GameplayTagContainer (Struct) | The container to search in |
  | `TagsToCheck` | GameplayTagContainer (Struct) | The tags to look for |
- **Output Pins:**
  | Name | Type | Description |
  |------|------|-------------|
  | `bHasAny` | Boolean | True if container has at least one of TagsToCheck |
- **Node-by-Node Logic:**
  ```
  [Macro Body: TagContainerHasAny] (PURE)
    Step 1: Call "Has Any" (Container, TagsToCheck) → bHasAny
      (on the GameplayTagContainer struct, search: "Has Any" or "Has Any Gameplay Tags")
    Step 2: Return bHasAny
  ```
- **Nodes to Search For:**
  - `Has Any` (GameplayTagContainer function)
- **One-Liner:** This is a direct wrapper around the built-in `Has Any` node on GameplayTagContainer. You can use `Has Any` directly without this macro.

---

### 4.5 Text Formatting Macros

#### Macro: `FormatTime`
- **Category:** `Framework|Utilities|Text`
- **Description:** Converts total seconds into a formatted time string. Formats as `HH:MM:SS` if >= 1 hour, otherwise `MM:SS`.
- **Input Pins:**
  | Name | Type | Description |
  |------|------|-------------|
  | `TotalSeconds` | Float | Total time in seconds |
- **Output Pins:**
  | Name | Type | Description |
  |------|------|-------------|
  | `FormattedText` | Text | Formatted time string |
- **Node-by-Node Logic:**
  ```
  [Macro Body: FormatTime] (PURE)
    Step 1: Call "Truncate" (TotalSeconds) → TotalInt (Integer)
    (or use "Floor" then "Integer to Integer64" if needed)
    
    Step 2: Integer division: Hours = TotalInt / 3600
    Step 3: Integer division: RemainingAfterHours = TotalInt % 3600
           Minutes = RemainingAfterHours / 60
           Seconds = RemainingAfterHours % 60
    
    Step 4: Convert each to String: HoursStr, MinutesStr, SecondsStr
      Use "Format Text" with {0}:D2 padding, or manually pad:
      - "To String (Integer)" + "Append"
      - Use "Cull" + "Branch" to check if < 10 and prepend "0"
    
    Step 5: Branch: Hours > 0
      True → "Format Text" ("{0}:{1}:{2}", HoursStr, PaddedMinutes, PaddedSeconds) → Return
      False → "Format Text" ("{0}:{1}", PaddedMinutes, PaddedSeconds) → Return
  ```
- **Nodes to Search For:**
  - `Truncate` (Float → Integer)
  - `/` (Integer division)
  - `%` (Modulo / remainder)
  - `To String (Integer)`
  - `Format Text`
  - `Branch`
- **Simplified Padding Approach:**
  ```
  To pad a number to 2 digits:
    Branch: Number < 10
      True → "Format Text" ("0{0}", NumberStr) → Padded
      False → Use NumberStr as-is → Padded
  ```
- **Full Implementation Detail (node by node):**
  ```
  Node 1: "Truncate" (Float: TotalSeconds) → TotalInt (Int)
  Node 2: "Int / Int" (TotalInt / 3600) → Hours (Int)
  Node 3: "Int % Int" (TotalInt % 3600) → RemainderHours (Int)
  Node 4: "Int / Int" (RemainderHours / 60) → Minutes (Int)
  Node 5: "Int % Int" (RemainderHours % 60) → Seconds (Int)
  
  Node 6: "To String (Integer)" (Hours) → HoursStr (String)
  Node 7: "To String (Integer)" (Minutes) → MinutesStr (String)
  Node 8: "To String (Integer)" (Seconds) → SecondsStr (String)
  
  // Pad minutes and seconds
  Node 9: "Int > Int" (Minutes < 10) → Branch
    True → "Format Text" ("0{0}", MinutesStr) → PaddedMin (Text)
    False → "To Text" (MinutesStr) → PaddedMin (Text)
  
  Node 10: Same for seconds → PaddedSec (Text)
  
  Node 11: "Int > Int" (Hours > 0) → Branch
    True → "Format Text" ("{0}:{1}:{2}", HoursStr, PaddedMin, PaddedSec) → FormattedText
    False → "Format Text" ("{0}:{1}", PaddedMin, PaddedSec) → FormattedText
  
  Return FormattedText
  ```

---

#### Macro: `Pluralise`
- **Category:** `Framework|Utilities|Text`
- **Description:** Returns singular or plural form based on count (count == 1 returns singular, otherwise plural).
- **Input Pins:**
  | Name | Type | Description |
  |------|------|-------------|
  | `Count` | Integer | The count |
  | `Singular` | Text | Singular form text |
  | `Plural` | Text | Plural form text |
- **Output Pins:**
  | Name | Type | Description |
  |------|------|-------------|
  | `Result` | Text | Singular or plural text |
- **Node-by-Node Logic:**
  ```
  [Macro Body: Pluralise] (PURE)
    Step 1: Branch: Count == 1
      True → Return Singular
      False → Return Plural
  ```
- **Nodes to Search For:**
  - `Equal (Integer)`
  - `Select Text` (condition → two Text options → output)
- **Quick Implementation:**
  ```
  "Equal (Integer)" (Count, 1) → bIsSingular (Bool)
  "Select (Text)" (bIsSingular, OptionA=Singular, OptionB=Plural) → Result
  Return Result
  ```

---

#### Macro: `OrdinalSuffix`
- **Category:** `Framework|Utilities|Text`
- **Description:** Returns the English ordinal suffix for a number: "st", "nd", "rd", or "th".
- **Input Pins:**
  | Name | Type | Description |
  |------|------|-------------|
  | `Number` | Integer | The number (e.g., 1, 2, 3, 4, 11, 12, 13, 21, 22, 23) |
- **Output Pins:**
  | Name | Type | Description |
  |------|------|-------------|
  | `Suffix` | String | The suffix ("st", "nd", "rd", "th") |
- **Node-by-Node Logic:**
  ```
  [Macro Body: OrdinalSuffix] (PURE)
    Step 1: Mod11 = Number % 100    // Get last two digits
    Step 2: Branch: (Mod11 >= 11 AND Mod11 <= 13)
      True → Return "th"    // Special case: 11th, 12th, 13th
    
    Step 3: Mod10 = Number % 10     // Get last digit
    Step 4: Switch on Mod10
      1 → Return "st"
      2 → Return "nd"
      3 → Return "rd"
      Default → Return "th"
  ```
- **Nodes to Search For:**
  - `%` (Integer modulo)
  - `And` (Boolean)
  - `>=`, `<=` (Integer comparison)
  - `Switch on Int` (or chain of `Select String` nodes)
  - `Select String`
- **Full Implementation Detail (node by node):**
  ```
  Node 1: "Int % Int" (Number % 100) → Mod11 (Int)
  Node 2: "Int >= Int" AND "Int <= Int" (Mod11 >= 11, Mod11 <= 13) → bSpecial (Bool)
  Node 3: If bSpecial → Return "th"
  Node 4: "Int % Int" (Number % 10) → Mod10 (Int)
  Node 5: "Switch on Int" (Mod10)
    Case 1 → Return "st"
    Case 2 → Return "nd"
    Case 3 → Return "rd"
    Default → Return "th"
  ```
  **If `Switch on Int` isn't available as a pure node**, use a chain of `Select String` nodes:
  ```
  "Equal (Integer)" (Mod10, 1) → "Select String" (True="st", False=next check)
  next check: "Equal (Integer)" (Mod10, 2) → "Select String" (True="nd", False=next check)
  next check: "Equal (Integer)" (Mod10, 3) → "Select String" (True="rd", False="th")
  ```

---

#### Macro: `TruncateText`
- **Category:** `Framework|Utilities|Text`
- **Description:** Truncates text to `MaxChars` characters with "..." ellipsis if truncated.
- **Input Pins:**
  | Name | Type | Description |
  |------|------|-------------|
  | `Input` | Text | The text to potentially truncate |
  | `MaxChars` | Integer | Maximum characters before truncation |
- **Output Pins:**
  | Name | Type | Description |
  |------|------|-------------|
  | `Output` | Text | Truncated text (or original if within limit) |
  | `bTruncated` | Boolean | True if text was truncated |
- **Node-by-Node Logic:**
  ```
  [Macro Body: TruncateText] (PURE)
    Step 1: Convert Input to String: "Text to String" (Input) → InputStr (String)
    Step 2: Call "Len" (InputStr) → CharCount (Integer)
    Step 3: Branch: CharCount > MaxChars
      True →
        Step 3a: Call "Left" (InputStr, MaxChars) → TruncatedStr (String)
        Step 3b: Call "Append" (TruncatedStr, "...") → FinalStr (String)
        Step 3c: Convert to Text: "String to Text" (FinalStr) → Output (Text)
        Step 3d: Set bTruncated = True
        Step 3e: Return Output, bTruncated
      False →
        Step 3f: Return Input (unchanged), bTruncated = False
  ```
- **Nodes to Search For:**
  - `Text to String`
  - `Len` (String length)
  - `Left` (String function — gets left N characters)
  - `Append` (String concatenation)
  - `String to Text`
  - `Select Text`
  - `Select Bool`
- **Quick Implementation:**
  ```
  "Text to String" (Input) → InputStr
  "Len" (InputStr) → CharCount
  "Int > Int" (CharCount > MaxChars) → bShouldTruncate (Bool)
  
  "Left" (InputStr, MaxChars) → TruncatedStr
  "Append" (TruncatedStr, "...") → FinalStr
  "String to Text" (FinalStr) → TruncatedText
  
  // Select final output
  "Select Text" (bShouldTruncate, True=TruncatedText, False=Input) → Output
  Return Output, bTruncated
  ```

---

### 4.6 Screen & Viewport Macros

#### Macro: `WorldToScreenSafe`
- **Category:** `Framework|Utilities|Screen`
- **Description:** Projects a world position to screen coordinates. Returns `False` if off-screen or behind camera.
- **Input Pins:**
  | Name | Type | Description |
  |------|------|-------------|
  | `exec` | Exec | Execution input |
  | `WorldContextObject` | Object | World context for camera |
  | `WorldLocation` | Vector | World position to project |
- **Output Pins:**
  | Name | Type | Description |
  |------|------|-------------|
  | `exec` | Exec | Execution output |
  | `ScreenPosition` | Vector2D | Screen position in pixels |
  | `bIsOnScreen` | Boolean | True if on screen and in front of camera |
- **Node-by-Node Logic:**
  ```
  [Macro Body: WorldToScreenSafe]
    Step 1: Call "Get Player Controller" (PlayerIndex=0) → PC (PlayerController)
    Step 2: Branch on "Is Valid" (PC)
      False → Return ScreenPosition=(0,0), bIsOnScreen=False
    
    Step 3: Call "Project World Location to Screen" (PC, WorldLocation, 
            bPlayerViewportRelative=False) → ScreenPos (Vector2D), bSuccess (Bool)
    
    Step 4: Return ScreenPos, bIsOnScreen=bSuccess
  ```
- **Nodes to Search For:**
  - `Get Player Controller`
  - `Project World Location to Screen`
  - `Is Valid`
  - `Branch`
- **Note:** `Project World Location to Screen` returns `False` if the point is behind the camera. The `bIsOnScreen` output only tells you if it's in front — to check if it's within the viewport bounds, use `ClampToViewport` after this macro.

---

#### Macro: `ClampToViewport`
- **Category:** `Framework|Utilities|Screen`
- **Description:** Clamps a widget position (Vector2D in screen pixel coordinates) to stay within the viewport bounds, with optional padding.
- **Input Pins:**
  | Name | Type | Description |
  |------|------|-------------|
  | `exec` | Exec | Execution input |
  | `WidgetPosition` | Vector2D | The position to clamp |
  | `Padding` | Float | Padding from viewport edges in pixels |
- **Output Pins:**
  | Name | Type | Description |
  |------|------|-------------|
  | `exec` | Exec | Execution output |
  | `ClampedPosition` | Vector2D | Clamped position |
- **Node-by-Node Logic:**
  ```
  [Macro Body: ClampToViewport]
    Step 1: Call "Get Viewport Size" → ViewportSize (Vector2D)
    Step 2: Clamp X:
      "Clamp Float" (WidgetPosition.X, Min=Padding, Max=ViewportSize.X - Padding) → ClampedX
    Step 3: Clamp Y:
      "Clamp Float" (WidgetPosition.Y, Min=Padding, Max=ViewportSize.Y - Padding) → ClampedY
    Step 4: Make Vector2D from ClampedX, ClampedY → ClampedPosition
    Step 5: Return ClampedPosition
  ```
- **Nodes to Search For:**
  - `Get Viewport Size`
  - `Clamp (Float)` — or `FClamp`
  - `Make Vector2D`
  - `Subtract Float - Float`
  - `Break Vector2D` (to get X and Y)

---

### 4.7 Debug Macros (Shipping-Safe)

These macros are automatically stripped from **Shipping** builds because the underlying engine nodes (`Print String`, `Draw Debug Sphere`, `Draw Debug String`) are no-ops in shipping builds. You wrap them with a `Branch: Is Editor Build` check for extra safety.

#### Macro: `LogDebug`
- **Category:** `Framework|Utilities|Debug`
- **Description:** Conditional debug output. Prints to output log AND viewport. Stripped in shipping builds.
- **Input Pins:**
  | Name | Type | Description |
  |------|------|-------------|
  | `exec` | Exec | Execution input |
  | `Category` | Name | Log category (any name) |
  | `Message` | String | Message to log |
  | `Color` | Linear Color | Text color on screen |
  | `Duration` | Float | Screen display duration (seconds, default 4.0) |
  | `WorldContextObject` | Object | World context |
- **Output Pins:**
  | Name | Type | Description |
  |------|------|-------------|
  | `exec` | Exec | Execution output |
- **Node-by-Node Logic:**
  ```
  [Macro Body: LogDebug]
    Step 1: Call "Is Editor Build" → bIsEditor (Bool)
      (search: "Is Editor Build" — development-only macro)
    
    Step 2: Branch on bIsEditor
      True →
        Step 2a: Call "Print String" (Message, TextColor=Color, Duration=Duration)
        Step 2b: Call "Log String" (Message, Category)
          (search: "Log String" — this goes to Output Log)
        Step 2c: Return
      False →
        Return (no-op in shipping)
  ```
- **Nodes to Search For:**
  - `Is Editor Build`
  - `Branch`
  - `Print String` (set `Screen Color` to the `Color` input)
  - `Log String` (blueprint node for `UE_LOG` equivalent — search "Log String" in Utilities)
- **Note:** If `Log String` doesn't appear in context menu, you can use `Print String` with `Print to Log` checked ✓ instead. Or use the `Log` node from the `Developer` category.

---

#### Macro: `DrawDebugSphere`
- **Category:** `Framework|Utilities|Debug`
- **Description:** Draws a debug sphere in the world. Stripped in shipping builds. **This is a wrapper around the built-in `Draw Debug Sphere` node.**
- **Input Pins:**
  | Name | Type | Description |
  |------|------|-------------|
  | `exec` | Exec | Execution input |
  | `WorldContextObject` | Object | World context |
  | `Center` | Vector | Sphere center world position |
  | `Radius` | Float | Sphere radius (default 50) |
  | `Color` | Linear Color | Sphere color (default green) |
  | `Duration` | Float | How long the sphere persists (default 5.0) |
  | `Thickness` | Float | Line thickness (default 0.0 = thin) |
- **Output Pins:**
  | Name | Type | Description |
  |------|------|-------------|
  | `exec` | Exec | Execution output |
- **Node-by-Node Logic:**
  ```
  [Macro Body: DrawDebugSphere]
    Step 1: Branch on "Is Editor Build"
      True →
        Call "Draw Debug Sphere" (Center, Radius, Segments=12, Color, Duration, Thickness)
        Return
      False →
        Return
  ```
- **Nodes to Search For:**
  - `Draw Debug Sphere`
  - `Is Editor Build`
  - `Branch`

---

#### Macro: `DrawDebugString3D`
- **Category:** `Framework|Utilities|Debug`
- **Description:** Draws 3D world-space debug text. Stripped in shipping builds. **Wrapper around the built-in `Draw Debug String` node.**
- **Input Pins:**
  | Name | Type | Description |
  |------|------|-------------|
  | `exec` | Exec | Execution input |
  | `WorldContextObject` | Object | World context |
  | `Location` | Vector | 3D world position for the text |
  | `Text` | String | Text to display |
  | `Color` | Linear Color | Text color (default white) |
  | `Duration` | Float | Display duration (default 4.0) |
  | `Scale` | Float | Font scale (default 1.0) |
- **Output Pins:**
  | Name | Type | Description |
  |------|------|-------------|
  | `exec` | Exec | Execution output |
- **Node-by-Node Logic:**
  ```
  [Macro Body: DrawDebugString3D]
    Step 1: Branch on "Is Editor Build"
      True →
        Call "Draw Debug String" (Location, Text, Color=nullptr (use default), Duration, Scale)
        (search: "Draw Debug String" — select the version with WorldContextObject pin)
        Return
      False →
        Return
  ```
- **Nodes to Search For:**
  - `Draw Debug String`
  - `Is Editor Build`
  - `Branch`
- **Note:** The built-in `Draw Debug String` node may have different pin names depending on your UE5 version (5.2+ vs 5.5+). Look for the variant that takes `WorldContextObject`, `Text Location`, `Draw Color`, and `Duration`.

---

## 5. Event Dispatchers

None. This is a Macro Library — no event dispatchers.

---

## 6. Overridden Events / Custom Events

None. Macros have no `BeginPlay` or lifecycle events.

---

## 7. Blueprint Graph Logic Flow

```mermaid
flowchart TD
    A[Any Blueprint calls ML_GameUtilities macro] --> B{Macro Type?}
    B -->|Pure Macro| C[Executes inline as part of caller's expression]
    B -->|Impure Macro| D[Receives exec pin flow]
    C --> E[Returns value to caller's node]
    D --> F[Checks input validity]
    F -->|Valid| G[Performs operation]
    F -->|Invalid| H[Returns None / False / no-op]
    G --> E
    H --> I[Outputs exec pin to continue]
```

Every macro is self-contained. The caller simply drops the node and wires inputs/outputs.

---

## 8. Communication Matrix

| Macro | Calls | Purpose |
|-------|-------|---------|
| `GetSubsystemSafe` | `Get Game Instance` → `Get Subsystem (Class)` | Safe subsystem lookup |
| `GetGameFramework` | `GetSubsystemSafe` or `Get Game Instance` → `Cast to GI_GameFramework` | Typed Game Instance access |
| `GetPlayerController` | `Get Player Controller` → `Cast to PC_CoreController` | Typed controller access |
| `FindComponentByInterface` | `Get Components by Class` + `Does Implement Interface` | Interface component search |
| `FindNearestActorWithTag` | `Sphere Overlap Actors` / `Get All Actors with Tag` | Tagged actor spatial search |
| `RemapFloat` | `Map Range Clamped` | Math remap |
| `LerpClamped` | `Clamp` + `Lerp` | Clamped interpolation |
| `VectorToAngle2D` | `Atan2` (degrees) | Direction → angle |
| `AngleDifference` | `%` modulo + branch | Shortest angle delta |
| `HasGameplayTag` | `Does Actor Have Tag` | Tag presence check |
| `AddGameplayTagToActor` | `Get Actor Gameplay Tag Container` + `Add Gameplay Tag` | Tag mutation |
| `RemoveGameplayTagFromActor` | `Get Actor Gameplay Tag Container` + `Remove Gameplay Tag` | Tag mutation |
| `MakeTagFromString` | `Make Literal GameplayTag` | String → tag conversion |
| `TagContainerHasAny` | `Has Any` (container function) | Tag container query |
| `FormatTime` | `Truncate`, `/`, `%`, `Format Text` | Time display |
| `Pluralise` | `Equal (Int)` + `Select Text` | Singular/plural |
| `OrdinalSuffix` | `% Mod`, `Select String` | "st"/"nd"/"rd"/"th" |
| `TruncateText` | `Len`, `Left`, `Append` | Text truncation |
| `WorldToScreenSafe` | `Project World Location to Screen` | Screen projection |
| `ClampToViewport` | `Get Viewport Size` + `Clamp Float` | Screen clamping |
| `LogDebug` | `Print String` + `Log String` (guarded by `Is Editor Build`) | Debug output |
| `DrawDebugSphere` | `Draw Debug Sphere` (guarded) | Debug sphere |
| `DrawDebugString3D` | `Draw Debug String` (guarded) | Debug 3D text |

---

## 9. Validation / Testing Checklist

- [ ] Macro Library created at `Content/Framework/Core/ML_GameUtilities`
- [ ] All 24 macros present and named correctly
- [ ] Pure macros (math, text, tag queries) have `Pure` checked ✓ in macro settings
- [ ] Impure macros (subsystem, actor, debug, screen) have exec input/output pins
- [ ] `GetSubsystemSafe` returns `None` instead of crashing when Subsystem doesn't exist
- [ ] `GetGameFramework` returns `None` when GameInstance is wrong type
- [ ] `GetPlayerController` returns `None` when no controller
- [ ] `FormatTime(3661.0)` returns `"1:01:01"` or `"01:01:01"`
- [ ] `FormatTime(125.0)` returns `"2:05"` (no hours)
- [ ] `Pluralise(1, "item", "items")` returns `"item"` and `Pluralise(3, "item", "items")` returns `"items"`
- [ ] `OrdinalSuffix(1)` = `"st"`, `OrdinalSuffix(2)` = `"nd"`, `OrdinalSuffix(11)` = `"th"`
- [ ] `TruncateText("Hello World", 5)` returns `"Hello..."`, `bTruncated=True`
- [ ] `HasGameplayTag` returns `False` for invalid actor (no crash)
- [ ] `RemapFloat(0.5, 0, 1, 0, 100)` returns `50.0`
- [ ] `LerpClamped(0, 100, 0.5)` returns `50.0`
- [ ] `VectorToAngle2D(1, 0)` returns `0.0`, `VectorToAngle2D(0, 1)` returns `90.0`
- [ ] `AngleDifference(10, 350)` returns `-20` (or `-20.0`)
- [ ] Debug macros produce output in Editor; no output in Shipping build (test `Is Editor Build` branch)
- [ ] `WorldToScreenSafe` returns `bIsOnScreen=False` for point behind camera
- [ ] `ClampToViewport` keeps position within viewport bounds
- [ ] `TagContainerHasAny` correctly reports OR logic for overlapping containers
- [ ] Edge case: `GetSubsystemSafe` with `None` WorldContextObject → returns `None` (no crash)
- [ ] Edge case: `FindComponentByInterface` on actor with zero components → returns `None`, `bFound=False`
- [ ] Edge case: `FormatTime(0)` returns `"0:00"`
- [ ] Edge case: `TruncateText("", 10)` returns `""`, `bTruncated=False`

---

## 10. Reuse Notes

- **This is the Blueprint-only replacement for `FL_GameUtilities`.** Use this when you can't compile C++.
- All macros use only built-in engine nodes. No plugins or custom C++ required.
- **When to use `ML_GameUtilities` vs raw engine nodes:**
  - Use `GetSubsystemSafe` whenever you need a null-safe subsystem lookup (instead of raw `Get Subsystem`).
  - Use `FormatTime` instead of writing your own time formatting logic.
  - For simple math like `RemapFloat`, you can use `Map Range Clamped` directly — the macro is just a convenience alias.
  - For debug macros, the `Is Editor Build` guard ensures zero shipping overhead.
- **Extending:** If you need project-specific utilities, create your own Macro Library (e.g., `ML_ProjectUtilities`) and add macros there. Do NOT modify `ML_GameUtilities` — it should remain a generic utility library.
- **Construction Script compatibility:** Pure macros can be called from construction scripts. Impure macros cannot (construction scripts don't support exec pins).
- **Macro vs Function:** Macros are "copy-pasted" into every caller graph at compile time. They have zero function call overhead but increase Blueprint graph size. For frequently-used macros (like `FormatTime`), this is fine. For very complex macros (`FindNearestActorWithTag`), consider creating a `BPC_` component with a function instead.

---

## 11. Manual Implementation Guide

> **This section is for a human implementer building this Macro Library manually in UE5.**
> Follow these steps IN ORDER. Each step creates one macro.

### 11.1 Class Setup

1. **Create the Macro Library asset:**
   - In the Content Browser, navigate to `Content/Framework/Core/`.
   - Right-click → **Miscellaneous** → **Blueprint Macro Library**.
   - Name it: `ML_GameUtilities`.
   - Double-click to open.

2. **Set up the macro categories:**
   - This step is optional but helps with organization.
   - In the Macro Library window, you'll see a list of macros on the left panel.
   - Macros will appear in the context menu under `Framework|Utilities|{Category}` based on what you type in the **Category** field of each macro.

### 11.2 Create Each Macro — Step-by-Step

For **each macro** listed below, follow this creation process:

1. In the `ML_GameUtilities` window, click **+ Add Macro** in the "My Blueprint" panel.
2. Name it exactly as specified.
3. In the **Details** panel, set:
   - **Category:** (as listed)
   - **Keywords:** (add search keywords like `utility`, `safe`, `subsystem`)
   - **Description:** (as listed)
4. For **pure** macros: check ✓ **Pure** in the macro's Details panel.
5. Add input and output pins (click the **+** button on the macro's header, or right-click → "Add Input" / "Add Output").
6. Open the macro graph and wire the nodes as described in each section.

#### Step 1: `GetSubsystemSafe` (Impure)

1. Add macro → name `GetSubsystemSafe`, Category = `Subsystems`.
2. Add input pins:
   - `WorldContextObject` — Type: `Object`
   - `SubsystemClass` — Type: `GameInstanceSubsystem` (Class Reference)
3. Add output pin: `Subsystem` — Type: `GameInstanceSubsystem`
4. In the graph:
   ```
   [Input exec] → [Get Game Instance] (WorldContextObject) → [Is Valid] → [Branch]
     True → [Get Subsystem (Class)] (GameInstance, SubsystemClass) → Return node → [Output Subsystem]
     False → Return node → leave Subsystem pin unconnected (returns None)
   ```
5. **Nodes to drop:** `Get Game Instance`, `Is Valid`, `Branch`, `Get Subsystem (Class)` (from the `Subsystems` submenu).

#### Step 2: `GetGameFramework` (Impure)

1. Add macro → name `GetGameFramework`, Category = `Subsystems`.
2. Add input pin: `WorldContextObject` — Type: `Object`.
3. Add output pin: `GameFramework` — Type: `GI_GameFramework` (or your actual game instance class).
4. In the graph:
   ```
   [Input exec] → [Get Game Instance] → [Cast to GI_GameFramework]
     Success → Return → [GameFramework]
     Failed → [Print String] (Warning: "No GameFramework") → Return → None
   ```
5. **Nodes to drop:** `Get Game Instance`, `Cast to GI_GameFramework`, `Print String`, `Branch`.

#### Step 3: `GetPlayerController` (Impure)

1. Add macro → name `GetPlayerController`, Category = `Subsystems`.
2. Add input pin: `WorldContextObject` — Type: `Object`.
3. Add output pin: `PlayerController` — Type: `PC_CoreController` (or your actual PC class).
4. In the graph:
   ```
   [Input exec] → [Get Player Controller] (Player Index 0) → [Cast to PC_CoreController]
     Success → Return → [PlayerController]
     Failed → Return → None
   ```
5. **Nodes to drop:** `Get Player Controller`, `Cast to PC_CoreController`, `Branch`.

#### Step 4: `GetSubsystemByClass` (Impure)

1. Add macro → name `GetSubsystemByClass`, Category = `Subsystems`.
2. Add input pins: `WorldContextObject` (Object), `SubsystemClass` (Class Ref → GameInstanceSubsystem).
3. Add output pin: `Subsystem` (GameInstanceSubsystem).
4. In the graph:
   ```
   [Input exec] → [GetSubsystemSafe macro] (WorldContextObject, SubsystemClass) → [Subsystem output]
   Return Subsystem
   ```
5. **Nodes to drop:** The `GetSubsystemSafe` macro itself (drag from "My Blueprint" panel into this graph).

#### Step 5: `FindComponentByInterface` (Impure)

1. Add macro → name `FindComponentByInterface`, Category = `Actor`.
2. Add input pins: `TargetActor` (Actor), `InterfaceClass` (Class Ref → Interface).
3. Add output pins: `FoundComponent` (ActorComponent), `bFound` (Boolean).
4. In the graph:
   - `Is Valid` (TargetActor) → `Branch` → False path: return None, bFound=False.
   - True path: `Get Components by Class` (TargetActor, ActorComponent) → `For Loop with Break`:
     - Loop body: `Does Implement Interface` (array element, InterfaceClass) → `Branch` → True: set FoundComponent, set bFound=True, break. False: continue.
   - After loop: return FoundComponent, bFound.
5. **Nodes to drop:** `Is Valid`, `Branch`, `Get Components by Class`, `For Loop with Break`, `Does Implement Interface`.

#### Step 6: `FindNearestActorWithTag` (Impure)

1. Add macro → name `FindNearestActorWithTag`, Category = `Actor`.
2. Add input pins: `Origin` (Vector), `Radius` (Float), `RequiredTag` (GameplayTag), `ActorsToIgnore` (Array of Actor).
3. Add output pins: `NearestActor` (Actor), `Distance` (Float).
4. In the graph:
   - `Sphere Overlap Actors` (Origin, Radius, Object Type: WorldDynamic or All) → Array of Actors.
   - Local variables: `NearestActor` (Actor, default None), `NearestDist` (Float, default -1 or a large number).
   - `For Each Loop` over OverlappedActors:
     - `Does Actor Have Tag` (Array Element, RequiredTag) → `Branch`:
       - True: Check if element is in `ActorsToIgnore` via `Array Contains Item` → skip if true.
       - If not ignored: `Get Distance To` (Element, Origin) → Dist.
       - Branch: Dist <= NearestDist OR NearestDist < 0 → set NearestActor & NearestDist.
     - False: continue.
   - Return NearestActor, NearestDist.
5. **Nodes to drop:** `Sphere Overlap Actors`, `For Each Loop`, `Does Actor Have Tag`, `Array Contains Item`, `Get Distance To`, `Branch`, `Print String` (warning).

#### Step 7: `RemapFloat` (Pure)

1. Add macro → name `RemapFloat`, Category = `Math`. Check ✓ **Pure**.
2. Add input pins: `Value`, `InMin`, `InMax`, `OutMin`, `OutMax` (all Float).
3. Add output pin: `Result` (Float).
4. In the graph:
   ```
   [Map Range Clamped] (In Value=Value, In Range A=InMin, In Range B=InMax, Out Range A=OutMin, Out Range B=OutMax, Clamped=True) → Result
   Return Result
   ```
5. **Nodes to drop:** `Map Range Clamped`.

#### Step 8: `LerpClamped` (Pure)

1. Add macro → name `LerpClamped`, Category = `Math`. Check ✓ **Pure**.
2. Add input pins: `A`, `B`, `Alpha` (all Float).
3. Add output pin: `Result` (Float).
4. In the graph:
   ```
   [Clamp (Float)] (Alpha, 0.0, 1.0) → ClampedAlpha
   [Lerp (Float)] (A, B, ClampedAlpha) → Result
   Return Result
   ```
5. **Nodes to drop:** `Clamp (Float)`, `Lerp (Float)`.

#### Step 9: `VectorToAngle2D` (Pure)

1. Add macro → name `VectorToAngle2D`, Category = `Math`. Check ✓ **Pure**.
2. Add input pin: `Direction` (Vector2D).
3. Add output pin: `Angle` (Float).
4. In the graph:
   ```
   [Break Vector2D] (Direction) → X, Y
   [Deg Atan2] (Y, X) → RawAngle (-180 to 180)
   // Normalize to 0-360:
   [Select Float] (RawAngle < 0, True=RawAngle + 360, False=RawAngle) → Angle
   Return Angle
   ```
5. **Nodes to drop:** `Break Vector2D`, `Deg Atan2` (or `Atan2 (degrees)`), `Select Float`, `Float + Float`.

#### Step 10: `AngleDifference` (Pure)

1. Add macro → name `AngleDifference`, Category = `Math`. Check ✓ **Pure**.
2. Add input pins: `AngleA`, `AngleB` (both Float).
3. Add output pin: `Difference` (Float).
4. In the graph:
   ```
   [Float B - Float A] → Diff
   [FMod] (Diff, 360.0) → WrappedDiff
   [Select Float] (WrappedDiff > 180.0, True=WrappedDiff - 360.0, False=
     [Select Float] (WrappedDiff < -180.0, True=WrappedDiff + 360.0, False=WrappedDiff)
   ) → Difference
   Return Difference
   ```
5. **Nodes to drop:** `Float - Float`, `FMod` (search "fmod" or "modulo"), `Select Float`.

#### Step 11: `HasGameplayTag` (Pure)

1. Add macro → name `HasGameplayTag`, Category = `Tags`. Check ✓ **Pure**.
2. Add input pins: `TargetActor` (Actor), `Tag` (GameplayTag, struct by value).
3. Add output pin: `bHasTag` (Boolean).
4. In the graph:
   ```
   [Is Valid] (TargetActor) → ValidBool
   [Does Actor Have Tag] (TargetActor, Tag) → HasTagBool
   [AND Boolean] (ValidBool, HasTagBool) → bHasTag
   Return bHasTag
   ```
5. **Nodes to drop:** `Is Valid`, `Does Actor Have Tag`, `AND Boolean`.
6. **Note:** You can actually skip `Is Valid` — `Does Actor Have Tag` on `None` returns `False` safely. The extra check is insurance.

#### Step 12: `AddGameplayTagToActor` (Impure)

1. Add macro → name `AddGameplayTagToActor`, Category = `Tags`.
2. Add input pins: `TargetActor` (Actor), `Tag` (GameplayTag).
3. Add no output pins (just exec).
4. In the graph:
   ```
   [Input exec] → [Is Valid] (TargetActor) → [Branch]
     True → [Get Actor Gameplay Tag Container] (TargetActor) → TagContainer
            → [Add Gameplay Tag] (TagContainer, Tag) → Return node
     False → [Print String] ("TargetActor is invalid") → Return node
   ```
5. **Nodes to drop:** `Is Valid`, `Branch`, `Get Actor Gameplay Tag Container`, `Add Gameplay Tag`, `Print String`.

#### Step 13: `RemoveGameplayTagFromActor` (Impure)

1. Same as Step 12 but use `Remove Gameplay Tag` instead of `Add Gameplay Tag`.
2. Name: `RemoveGameplayTagFromActor`.

#### Step 14: `MakeTagFromString` (Pure)

1. Add macro → name `MakeTagFromString`, Category = `Tags`. Check ✓ **Pure**.
2. Add input pin: `TagString` (String).
3. Add output pin: `Tag` (GameplayTag).
4. In the graph:
   ```
   [String to Name] (TagString) → TagName (Name)
   [Make GameplayTag from Name] (TagName) → Tag
   Return Tag
   ```
5. **Nodes to drop:** `String to Name`, `Make GameplayTag from Name`.
6. **Alternative:** Use `Make Literal GameplayTag` directly — it takes a `Name` not a `String`. You may need to convert String→Name first.

#### Step 15: `TagContainerHasAny` (Pure)

1. Add macro → name `TagContainerHasAny`, Category = `Tags`. Check ✓ **Pure**.
2. Add input pins: `Container` (GameplayTagContainer), `TagsToCheck` (GameplayTagContainer).
3. Add output pin: `bHasAny` (Boolean).
4. In the graph:
   ```
   [Has Any] (Target=Container, Other=TagsToCheck) → bHasAny
   Return bHasAny
   ```
5. **Nodes to drop:** `Has Any` (search on GameplayTagContainer → "Has Any Gameplay Tags").

#### Step 16: `FormatTime` (Pure)

1. Add macro → name `FormatTime`, Category = `Text`. Check ✓ **Pure**.
2. Add input pin: `TotalSeconds` (Float).
3. Add output pin: `FormattedText` (Text).
4. In the graph — follow the detailed node-by-node logic from Section 4.5:
   ```
   1. Truncate(TotalSeconds) → TotalInt
   2. TotalInt / 3600 → Hours
   3. TotalInt % 3600 → Remainder
   4. Remainder / 60 → Minutes
   5. Remainder % 60 → Seconds
   6. To String (Hours) → HStr
   7. To String (Minutes) → MStr
   8. To String (Seconds) → SStr
   9. Pad MStr and SStr (Branch < 10 → prepend "0")
   10. Hours > 0 → Format Text("{0}:{1}:{2}", HStr, PaddedM, PaddedS)
       Else → Format Text("{0}:{1}", PaddedM, PaddedS)
   ```
5. **Key nodes:** `Truncate`, `/` (int division), `%` (mod), `To String (Integer)`, `Format Text`, `Select Text`.

#### Step 17: `Pluralise` (Pure)

1. Add macro → name `Pluralise`, Category = `Text`. Check ✓ **Pure**.
2. Inputs: `Count` (Integer), `Singular` (Text), `Plural` (Text).
3. Outputs: `Result` (Text).
4. Graph: `Equal (Int)` (Count, 1) → `Select Text` (True=Singular, False=Plural) → Return.

#### Step 18: `OrdinalSuffix` (Pure)

1. Add macro → name `OrdinalSuffix`, Category = `Text`. Check ✓ **Pure**.
2. Input: `Number` (Integer). Output: `Suffix` (String).
3. Follow Section 4.5 node-by-node: %100 → check 11-13 → %10 → switch on 1/2/3 → return.
4. **Key nodes:** `%` (mod), `AND (bool)`, `>=`, `<=`, `Select String` (chained).

#### Step 19: `TruncateText` (Pure)

1. Add macro → name `TruncateText`, Category = `Text`. Check ✓ **Pure**.
2. Inputs: `Input` (Text), `MaxChars` (Integer). Outputs: `Output` (Text), `bTruncated` (Boolean).
3. Follow Section 4.5: Text→String → Len → Branch (Len > MaxChars) → Left(str, MaxChars) + "..." → String→Text → Select Text.

#### Step 20: `WorldToScreenSafe` (Impure)

1. Add macro → name `WorldToScreenSafe`, Category = `Screen`.
2. Inputs: `WorldContextObject` (Object), `WorldLocation` (Vector).
3. Outputs: `exec`, `ScreenPosition` (Vector2D), `bIsOnScreen` (Boolean).
4. Graph: `Get Player Controller` → `Is Valid` → `Branch` → `Project World Location to Screen` → Return.
5. **Nodes:** `Get Player Controller`, `Is Valid`, `Branch`, `Project World Location to Screen`.

#### Step 21: `ClampToViewport` (Impure)

1. Add macro → name `ClampToViewport`, Category = `Screen`.
2. Inputs: `WidgetPosition` (Vector2D), `Padding` (Float).
3. Outputs: `exec`, `ClampedPosition` (Vector2D).
4. Graph: `Get Viewport Size` → `Break Vector2D` → Clamp X, Clamp Y each with `Padding` min and `ViewportSize - Padding` max → `Make Vector2D` → Return.
5. **Nodes:** `Get Viewport Size`, `Break Vector2D`, `Clamp (Float)`, `Make Vector2D`.

#### Step 22: `LogDebug` (Impure)

1. Add macro → name `LogDebug`, Category = `Debug`.
2. Inputs: `Category` (Name), `Message` (String), `Color` (LinearColor), `Duration` (Float, default 4.0), `WorldContextObject` (Object).
3. No output pins (just exec).
4. Graph: `Is Editor Build` → `Branch` → True: `Print String` (Message, TextColor=Color, Duration=Duration) + `Log String` (Message, Category) → Return. False: Return.

#### Step 23: `DrawDebugSphere` (Impure)

1. Add macro → name `DrawDebugSphere`, Category = `Debug`.
2. Inputs: `WorldContextObject` (Object), `Center` (Vector), `Radius` (Float, default 50), `Color` (LinearColor, default Green), `Duration` (Float, default 5.0), `Thickness` (Float, default 0.0).
3. Graph: `Is Editor Build` → `Branch` → True: `Draw Debug Sphere` → Return.

#### Step 24: `DrawDebugString3D` (Impure)

1. Add macro → name `DrawDebugString3D`, Category = `Debug`.
2. Inputs: `WorldContextObject` (Object), `Location` (Vector), `Text` (String), `Color` (LinearColor, default White), `Duration` (Float, default 4.0), `Scale` (Float, default 1.0).
3. Graph: `Is Editor Build` → `Branch` → True: `Draw Debug String` → Return.

---

### 11.3 Quick Node Reference

Common UE5 Blueprint nodes used across macros:

| Node | Where to Find | Used In |
|------|---------------|---------|
| `Get Game Instance` | Right-click → "Get Game Instance" | Subsystem macros |
| `Get Subsystem (Class)` | Context menu → Subsystems | `GetSubsystemSafe` |
| `Cast To` | Right-click → "Cast To {ClassName}" | `GetGameFramework`, `GetPlayerController` |
| `Get Player Controller` | Right-click → "Get Player Controller" (Player Index 0) | `GetPlayerController`, `WorldToScreenSafe` |
| `Is Valid` | Right-click → "Is Valid" (Object) | Nearly all macros |
| `Branch` | Right-click → "Branch" | All impure macros |
| `Map Range Clamped` | Right-click → "Map Range Clamped" | `RemapFloat` |
| `Clamp (Float)` | Right-click → "Clamp (Float)" | `LerpClamped`, `ClampToViewport` |
| `Lerp (Float)` | Right-click → "Lerp (Float)" | `LerpClamped` |
| `Deg Atan2` | Right-click → "Deg Atan2" | `VectorToAngle2D` |
| `FMod` | Right-click → "FMod" or "% (Float)" | `AngleDifference` |
| `Does Actor Have Tag` | Right-click → "Does Actor Have Tag" | `HasGameplayTag`, `FindNearestActorWithTag` |
| `Get Actor Gameplay Tag Container` | Right-click → "Get Actor Gameplay Tag Container" | Tag mutation macros |
| `Make Literal GameplayTag` | Right-click → "Make Literal GameplayTag" | `MakeTagFromString` |
| `Has Any` | Right-click on GameplayTagContainer → "Has Any" | `TagContainerHasAny` |
| `For Loop with Break` | Right-click → "For Loop with Break" | `FindComponentByInterface` |
| `Get Components by Class` | Right-click → "Get Components by Class" | `FindComponentByInterface` |
| `Does Implement Interface` | Right-click → "Does Implement Interface" | `FindComponentByInterface` |
| `Sphere Overlap Actors` | Right-click → "Sphere Overlap Actors" | `FindNearestActorWithTag` |
| `Get Distance To` | Right-click on Actor → "Get Distance To" | `FindNearestActorWithTag` |
| `Truncate` | Right-click → "Truncate (Float)" | `FormatTime` |
| `Format Text` | Right-click → "Format Text" | `FormatTime` |
| `Select Text` | Right-click → "Select" | `Pluralise`, `TruncateText` |
| `Select Float` | Right-click → "Select" | `AngleDifference`, `VectorToAngle2D` |
| `Select String` | Right-click → "Select" | `OrdinalSuffix` |
| `Left` (String) | Right-click → "Left (String)" | `TruncateText` |
| `Len` | Right-click → "Len" | `TruncateText` |
| `Project World Location to Screen` | Right-click → "Project World Location to Screen" | `WorldToScreenSafe` |
| `Get Viewport Size` | Right-click → "Get Viewport Size" | `ClampToViewport` |
| `Is Editor Build` | Right-click → "Is Editor Build" | All debug macros |
| `Print String` | Right-click → "Print String" | `LogDebug` |
| `Draw Debug Sphere` | Right-click → "Draw Debug Sphere" | `DrawDebugSphere` |
| `Draw Debug String` | Right-click → "Draw Debug String" | `DrawDebugString3D` |

---

## 12. Blueprint Build Checklist

Complete these steps in order when implementing this Macro Library:

- [ ] Create Macro Library asset at `Content/Framework/Core/ML_GameUtilities`
- [ ] Create `GetSubsystemSafe` macro (Subsystems, impure) — 3 ins, 1 out
- [ ] Create `GetGameFramework` macro (Subsystems, impure) — 1 in, 1 out
- [ ] Create `GetPlayerController` macro (Subsystems, impure) — 1 in, 1 out
- [ ] Create `GetSubsystemByClass` macro (Subsystems, impure) — wraps `GetSubsystemSafe`
- [ ] Create `FindComponentByInterface` macro (Actor, impure) — 2 ins, 2 outs
- [ ] Create `FindNearestActorWithTag` macro (Actor, impure) — 4 ins, 2 outs
- [ ] Create `RemapFloat` macro (Math, pure ✓) — 5 ins, 1 out
- [ ] Create `LerpClamped` macro (Math, pure ✓) — 3 ins, 1 out
- [ ] Create `VectorToAngle2D` macro (Math, pure ✓) — 1 in, 1 out
- [ ] Create `AngleDifference` macro (Math, pure ✓) — 2 ins, 1 out
- [ ] Create `HasGameplayTag` macro (Tags, pure ✓) — 2 ins, 1 out
- [ ] Create `AddGameplayTagToActor` macro (Tags, impure) — 2 ins
- [ ] Create `RemoveGameplayTagFromActor` macro (Tags, impure) — 2 ins
- [ ] Create `MakeTagFromString` macro (Tags, pure ✓) — 1 in, 1 out
- [ ] Create `TagContainerHasAny` macro (Tags, pure ✓) — 2 ins, 1 out
- [ ] Create `FormatTime` macro (Text, pure ✓) — 1 in, 1 out
- [ ] Create `Pluralise` macro (Text, pure ✓) — 3 ins, 1 out
- [ ] Create `OrdinalSuffix` macro (Text, pure ✓) — 1 in, 1 out
- [ ] Create `TruncateText` macro (Text, pure ✓) — 2 ins, 2 outs
- [ ] Create `WorldToScreenSafe` macro (Screen, impure) — 2 ins, 3 outs (2 data + exec)
- [ ] Create `ClampToViewport` macro (Screen, impure) — 2 ins, 2 outs (1 data + exec)
- [ ] Create `LogDebug` macro (Debug, impure) — 5 ins
- [ ] Create `DrawDebugSphere` macro (Debug, impure) — 5 ins
- [ ] Create `DrawDebugString3D` macro (Debug, impure) — 6 ins
- [ ] Run validation checklist (Section 9) — all 24 items pass
- [ ] Test from a test Blueprint Actor: call each macro and verify outputs
- [ ] Save macro library

---

## Multiplayer Networking

**Replication: None needed.** Macro Libraries are called locally per-client or server. The macros themselves have no state and make no RPCs. Functions like `GetPlayerController` return the local player controller on clients and the first player controller on the server (for the server's "local" player).

**Authority notes:**
- Subsystem access macros return local-side subsystems. Server-only subsystems won't be available on clients.
- `FindNearestActorWithTag` and `AddGameplayTagToActor` operate on whatever actors exist on the calling side. On a dedicated server, all actors exist. On a client, only replicated actors exist.

---

*Blueprint-only Macro Library specification for ML_GameUtilities — 24 macros, zero C++ required.*
