top of page

Test Blueprints for faults (in compiling graphs)

  • Tom Mooney
  • Jul 6
  • 4 min read

A blueprint can compile but still have faults. A lot of the faults I'm targeting are when copying BP graphs between actors in different projects; I'm very careful with it, but it's nice to have a checker. One fault is when a Timeline's output float (the curve created to control the playback) no longer exists or is not connected to a Lerp Alpha, so it plays but doesn't update anything. This can occur when a Timeline has been pasted from graph to graph and the Timeline internals reset, cutting the float output. Besides it can be easy to forget to hook up the float connection.

I tried getting chatgpt to write a test all of a project's graphs with a pythonscript pasted in the editor's Python command line, but it kept erroring on accessing the UberGraphPage (and I didn't want to write a C++ helper this time); instead, I settled for copying each graph manually to notepad++ and saving it as Desktop/Sample.txt and running pythonscript in IDLE. Surprisingly, this worked pretty well. I say it's manually, but it's few keystrokes and the script completes immediately, no matter how big the graph is. So I poked chatgpt to provide some tests for - If Timeline output pin of type float isn't connected to anything - If Lerp has nothing connected to its Alpha - If Branch node bool input value isn't connected to anything (this is sometimes legit, but it's good to test for) - If Branch node bool exec outputs (True/False) aren't connected to anything - If Branch node bool exec ouputs are both connected to the same input on another node (may rarely occur)


The image below shows cases of Branch that are incorrect but might not stop a graph from compiling:


The tests list the occurences of each 'fault' and also notes when everything looks alright. Chatgpt decided to add the icons (I don't see why not). === RESTART: C:/Users/ipad4/Desktop/Python/CheckSampleForRiskyBranchNodes.py ===

📋 Checking Blueprint graph: /Game/4P/Blueprints/ActorBP/BP_PillarPuzzle.BP_PillarPuzzle

🔍 Found 4 Branch node(s)

⚠️ Branches with unconnected exec input: 1

⚠️ Branches with unconnected Condition input: 1

⚠️ Branches with both outputs unconnected: 1

⚠️ Branches with redundant outputs to same target: 1

After fixes: === RESTART: C:/Users/ipad4/Desktop/Python/CheckSampleForRiskyBranchNodes.py ===

📋 Checking Blueprint graph: /Game/4P/Blueprints/ActorBP/BP_PillarPuzzle.BP_PillarPuzzle

🔍 Found 4 Branch node(s)

⚠️ Branches with unconnected exec input: 0

⚠️ Branches with unconnected Condition input: 0

⚠️ Branches with both outputs unconnected: 0

⚠️ Branches with redundant outputs to same target: 0

✅ All Branch nodes pass checks.


#!/usr/bin/env python3

import re

import sys

import os

from pathlib import Path


# Hard-coded path on the Desktop

desktop = Path(os.path.expanduser("~")) / "Desktop"

dump_file = desktop / "sample.txt"


# Regex to capture graph path from ExportPath

graph_pat = re.compile(r'ExportPath="[^\"]*?/Game(?P<path>[^:"]+)')


# Regex to capture each default Branch node block

branch_node_pat = re.compile(

r"Begin Object Class=/Script/BlueprintGraph\.(?:K2Node_IfThenElse|K2Node_Branch)\b(.*?)End Object",

re.DOTALL

)


def extract_pin_props(body):

"""

Extract full pin property blocks from body by manual parenthesis matching.

Returns a list of pin_props strings (inside parentheses).

"""

props = []

marker = 'CustomProperties Pin ('

idx = 0

while True:

start = body.find(marker, idx)

if start == -1:

break

paren = body.find('(', start + len(marker) - 1)

depth = 1

i = paren + 1

while i < len(body) and depth:

if body[i] == '(': depth += 1

elif body[i] == ')': depth -= 1

i += 1

if depth == 0:

props.append(body[paren+1:i-1])

idx = i

return props



def get_targets(pin_block):

"""

Given a pin property string, return a set of downstream node names from LinkedTo.

"""

m = re.search(r'LinkedTo=\(([^)]+)\)', pin_block)

if not m:

return set()

entries = [e.strip() for e in m.group(1).split(',') if e.strip()]

return set(entry.split()[0] for entry in entries)



def check_default_branches(text):

"""

Returns total branches and counts of nodes with:

- unconnected exec input

- unconnected Condition input

- both outputs unconnected

- redundant outputs to same targets

"""

branches = branch_node_pat.findall(text)

total = len(branches)


unconn_exec = 0

unconn_input = 0

unconn_output = 0

redundant = 0


for block in branches:

pins = extract_pin_props(block)


# Exec input missing (PinName="execute")

if any(

'PinName="execute"' in pin and

'PinType.PinCategory="exec"' in pin and

'LinkedTo=(' not in pin

for pin in pins

):

unconn_exec += 1


# Condition input missing

if any(

'PinName="Condition"' in pin and

'PinType.PinCategory="bool"' in pin and

'LinkedTo=(' not in pin

for pin in pins

):

unconn_input += 1


# then/else outputs

then_pins = [pin for pin in pins if 'PinName="then"' in pin]

else_pins = [pin for pin in pins if 'PinName="else"' in pin]

then_ok = any('LinkedTo=(' in pin for pin in then_pins)

else_ok = any('LinkedTo=(' in pin for pin in else_pins)


if not (then_ok or else_ok):

unconn_output += 1


if then_ok and else_ok:

then_targets = set().union(*(get_targets(pin) for pin in then_pins))

else_targets = set().union(*(get_targets(pin) for pin in else_pins))

if then_targets & else_targets:

redundant += 1


return total, unconn_exec, unconn_input, unconn_output, redundant



def main():

if not dump_file.exists():

print(f"Error: could not find {dump_file}", file=sys.stderr)

sys.exit(1)


text = dump_file.read_text(encoding='utf-8')


# Graph info

gm = graph_pat.search(text)

if gm:

print(f"📋 Checking Blueprint graph: /Game{gm.group('path')}")


b_tot, b_exec, b_in, b_out, b_red = check_default_branches(text)

print(f"🔍 Found {b_tot} Branch node(s)")

print(f"⚠️ Branches with unconnected exec input: {b_exec}")

print(f"⚠️ Branches with unconnected Condition input: {b_in}")

print(f"⚠️ Branches with both outputs unconnected: {b_out}")

print(f"⚠️ Branches with redundant outputs to same target: {b_red}")


if not (b_exec or b_in or b_out or b_red):

print("✅ All Branch nodes pass checks.")

sys.exit(0)

sys.exit(1)


if __name__ == '__main__':

main()


In IDLE, the logging looks more like so (for the Timeline checker example):

Regex expressions written by AI are never entirely reliable since the text the script runs over isn't necessarily following all the assumptions that a test case might have included. I tested what I was hoping to get working, but there's many ways to break such a brittle checker. However, for a few minutes chattin', it still contributes some saved time in finding mistakes; I have to fix the mistakes, but at least there's a shortcut to finding them.

 
 
 

Comments


Featured Posts
Recent Posts
Search By Tags
Follow Us
  • Facebook Basic Square
  • Twitter Basic Square
  • Google+ Basic Square

Follow Tom

  • gfds.de-bluesky-butterfly-logo
  • facebook-square

​Copyright 2014 and beyond and beforehand

bottom of page