# Advanced Scripting Techniques for Custom Node Graph Logic in Pyrite64

> Unlock advanced scripting in Pyrite64's node graph editor. Subclass Node::Base, implement generateCode, and register types to create custom C++ logic compiled directly into N64 binaries.

- Repository: [Max Bebök/pyrite64](https://github.com/hailtododongo/pyrite64)
- Tags: advanced-techniques
- Published: 2026-02-19

---

**You can extend Pyrite64's visual editor by subclassing `Node::Base`, implementing the `generateCode()` method to emit C++ fragments, and registering new types in the `NODE_TABLE` registry to create runtime-editable gameplay logic that compiles directly into N64 binaries.**

Pyrite64 ships with a flexible **Node Graph** system that enables visual scripting for gameplay logic, shader pipelines, and scene flow without writing C++ directly. By leveraging the architecture in `src/project/graph` and the script-generation pipeline in [`data/scripts/scriptTable.cpp`](https://github.com/HailToDodongo/pyrite64/blob/main/data/scripts/scriptTable.cpp), you can author **custom node types** that persist as JSON and compile into production-ready Nintendo 64 code. This guide covers the advanced techniques for extending the node graph, from variable resolution to conditional code generation.

## Architecture of the Node Graph System

The editor’s graph system is built on a layered architecture that separates UI, serialization, and code generation. Understanding these layers is essential before implementing custom logic.

The **base node class** in [`src/project/graph/nodes/baseNode.h`](https://github.com/HailToDodongo/pyrite64/blob/main/src/project/graph/nodes/baseNode.h) provides the common API for all nodes, including methods like `addInPin()`, `addOutPin()`, `serialize()`, and `deserialize()`. Concrete implementations such as `Node::Func` and `Node::Compare` in [`src/project/graph/nodes/nodeFunc.h`](https://github.com/HailToDodongo/pyrite64/blob/main/src/project/graph/nodes/nodeFunc.h) handle specific behaviors and code emission. The **graph manager** ([`src/project/graph/graph.h`](https://github.com/HailToDodongo/pyrite64/blob/main/src/project/graph/graph.h)) stores the node collection and connections, while the **node-type registry** in [`src/project/graph/graph.cpp`](https://github.com/HailToDodongo/pyrite64/blob/main/src/project/graph/graph.cpp) maintains a static `NODE_TABLE` that maps type IDs to factory lambdas for dynamic instantiation. Finally, the **script-builder** ([`data/scripts/scriptTable.cpp`](https://github.com/HailToDodongo/pyrite64/blob/main/data/scripts/scriptTable.cpp)) walks the saved graph at build time, invoking each node’s `generateCode()` method to produce the final C++ source.

## Creating a Custom Node from Scratch

To add a new node type, you must subclass the base class, implement the UI and generation logic, and register the type in the factory table.

### Step 1: Declare the Node Class

Create a new header file under `src/project/graph/nodes/` that inherits from `Node::Base`. Define a unique `TYPE_ID` and implement the required overrides.

```cpp
// src/project/graph/nodes/nodeMyLogic.h
#pragma once
#include "baseNode.h"

namespace Project::Graph::Node {

class MyLogic : public Base {
public:
    static constexpr const char* NAME = "MyLogic";
    static constexpr uint32_t TYPE_ID = 0x0100;   // Use unique ID < 0x8000

    MyLogic(ImFlow::ImNodeFlow& flow, const ImVec2& pos);
    void drawNode() override;
    void generateCode(std::ostream& out) const override;

private:
    int multiplier = 1;
};

} // namespace Project::Graph::Node

```

### Step 2: Implement UI and Code Generation

Implement the constructor to set up pins and styling, `drawNode()` for the ImGui interface, and `generateCode()` to emit C++.

```cpp
// src/project/graph/nodes/nodeMyLogic.cpp
#include "nodeMyLogic.h"
#include "ImNodeFlow.h"
#include <sstream>

namespace Project::Graph::Node {

MyLogic::MyLogic(ImFlow::ImNodeFlow& flow, const ImVec2& pos) {
    setPos(pos);
    setName(NAME);
    setStyle(std::make_shared<ImFlow::NodeStyle>(
        IM_COL32(0x88, 0x00, 0xCC, 0xFF), 
        ImColor(255, 255, 255, 255), 
        4.0f
    ));

    addInPin("in", ImFlow::Pin::Type::Int);
    addOutPin("out", ImFlow::Pin::Type::Int);
}

void MyLogic::drawNode() {
    ImGui::Text("Multiplier:");
    ImGui::InputInt("##mult", &multiplier);
    Base::drawNode();  // Renders pins
}

void MyLogic::generateCode(std::ostream& out) const {
    std::string srcVar = resolvePinVariable(0);  // Input pin index 0
    out << "int " << varName() << " = " << srcVar << " * " << multiplier << ";\n";
}

} // namespace Project::Graph::Node

```

**Key API methods:**
- **`resolvePinVariable(pinIndex)`**: Returns the C++ variable name feeding the specified input pin at generation time.
- **`varName()`**: Provides a unique identifier derived from the node’s UUID for naming generated variables.

### Step 3: Register in NODE_TABLE

Add your node to the factory registry in [`src/project/graph/graph.cpp`](https://github.com/HailToDodongo/pyrite64/blob/main/src/project/graph/graph.cpp) to enable instantiation from the editor palette and deserialization.

```cpp
// Inside NODE_TABLE definition in src/project/graph/graph.cpp
{ Node::MyLogic::TYPE_ID,
  { Node::MyLogic::NAME,
    [](ImFlow::ImNodeFlow& m, const ImVec2& pos) {
        return std::make_shared<Node::MyLogic>(m, pos);
    } 
  } 
},

```

After recompiling the editor, the node appears in the **Create** menu and participates in the build pipeline.

## Advanced Code Generation Patterns

Once you have basic nodes working, you can implement sophisticated scripting techniques using the following patterns.

### Reusing Computed Variables Across Outputs

When a node performs expensive calculations that multiple outputs need, emit a temporary variable and reference it for each pin.

```cpp
void MyLogic::generateCode(std::ostream& out) const override {
    std::string src = resolvePinVariable(0);
    std::string tmp = varName() + "_tmp";
    
    out << "int " << tmp << " = heavyComputation(" << src << ");\n";
    out << "int " << pinVarName(0) << " = " << tmp << ";\n";      // First output
    out << "int " << pinVarName(1) << " = " << tmp << " * 2;\n";  // Second output
}

```

**`pinVarName(pinIdx)`** generates the specific variable name for an output pin, allowing other nodes to reference it via `resolvePinVariable()`.

### Conditional Branch Generation

Implement comparison or branching logic by emitting `if` statements based on node state. The `Node::Compare` type demonstrates this pattern:

```cpp
void Compare::generateCode(std::ostream& out) const override {
    std::string lhs = resolvePinVariable(0);
    std::string rhs = resolvePinVariable(1);
    out << "bool " << varName() << " = (" << lhs << " " << opString << " " << rhs << ");\n";
}

```

Store the operator string (e.g., `"=="`, `"<="`) as a member variable set through a combo box in `drawNode()`.

### Accessing Global Script Variables

Query the global variable table during code generation to interact with shared state like player scores or game flags.

```cpp
void GlobalRead::generateCode(std::ostream& out) const override {
    std::string globalVar = ctx.globalVar("PlayerHealth", "int");
    out << "int " << varName() << " = " << globalVar << ";\n";
}

```

Add UI controls in `drawNode()` using `ImGui::InputText` to let users select which global variable to access.

### Custom Serialization for Complex State

If your node stores data structures like string tables or enums, override the serialization methods to persist them in the project JSON.

```cpp
void MyLogic::serialize(nlohmann::json& j) const {
    Base::serialize(j);
    j["operator"] = opString;
    j["values"] = valueArray;
}

void MyLogic::deserialize(const nlohmann::json& j) {
    Base::deserialize(j);
    opString = j.value("operator", "==");
    valueArray = j["values"].get<std::vector<int>>();
}

```

The graph loader in `Graph::load()` automatically invokes these methods when opening projects.

### Nesting Graphs as Sub-Functions

Pyrite64 supports **modular logic** through the `Func` node defined in [`src/project/graph/nodes/nodeFunc.h`](https://github.com/HailToDodongo/pyrite64/blob/main/src/project/graph/nodes/nodeFunc.h). To create reusable sub-routines:

1. Create a separate **NodeGraph** asset via **Assets → New → NodeGraph**.
2. In your main graph, place a **Func** node and assign the sub-graph asset.
3. The `Func` node’s `generateCode()` automatically emits a function call to the generated C++ function, mapping input pins to arguments and output pins to return values.

This technique is ideal for **behavior trees**, **audio event pipelines**, and reusable utility functions that appear across multiple levels.

## Build Integration and Debugging

When you press **Build**, the script-builder iterates over the serialized graph stored in the project JSON. It calls `generateCode()` on each node and writes the output to [`data/scripts/scriptTable.cpp`](https://github.com/HailToDodongo/pyrite64/blob/main/data/scripts/scriptTable.cpp). The build system then compiles this generated source into the final N64 binary.

To debug your generated logic:
- Inspect [`data/scripts/generatedNodeGraph.cpp`](https://github.com/HailToDodongo/pyrite64/blob/main/data/scripts/generatedNodeGraph.cpp) after a build to view the exact C++ output.
- Ensure your graph remains **acyclic**; the generator performs no topological sorting, so circular dependencies create forward-reference compiler errors.
- Keep `TYPE_ID` values below `0x8000` to avoid collisions with built-in engine nodes.

## Summary

- **Subclass `Node::Base`** in `src/project/graph/nodes/` to define custom node types with unique `TYPE_ID` values.
- **Implement `generateCode()`** to emit C++ fragments using `resolvePinVariable()` for inputs and `varName()` for local identifiers.
- **Register nodes** in the `NODE_TABLE` inside [`src/project/graph/graph.cpp`](https://github.com/HailToDodongo/pyrite64/blob/main/src/project/graph/graph.cpp) to enable editor integration.
- **Use advanced patterns** like `pinVarName()` for multi-output nodes, conditional emission for branching logic, and global variable access for shared state.
- **Leverage `Func` nodes** to nest graphs as reusable sub-functions, enabling complex modular architectures.

## Frequently Asked Questions

### How do I debug code generation errors in my custom nodes?

After triggering a build, open [`data/scripts/generatedNodeGraph.cpp`](https://github.com/HailToDodongo/pyrite64/blob/main/data/scripts/generatedNodeGraph.cpp) to inspect the exact C++ code emitted by your nodes. If the N64 compiler reports errors, check that `resolvePinVariable()` indices match the order of `addInPin()` calls in your constructor, and verify that no circular connections exist in the graph topology.

### Can I create nodes that output multiple data types?

Yes. Call `addOutPin()` multiple times with different `ImFlow::Pin::Type` values (e.g., `Int`, `Float`, `Bool`). In `generateCode()`, use `pinVarName(0)`, `pinVarName(1)`, etc., to declare separate variables for each output pin, allowing downstream nodes to reference them by type.

### What is the maximum number of custom nodes I can register?

The `TYPE_ID` field is a `uint32_t`, but you should reserve values below `0x8000` for custom types to avoid conflicts with engine-internal nodes. This provides approximately 32,767 unique identifiers for project-specific logic, which is sufficient for even large-scale N64 projects.

### How do I share custom nodes between different Pyrite64 projects?

Since nodes are standard C++ classes in `src/project/graph/nodes/`, you can copy the header and implementation files into another project’s source tree. Ensure you also copy the corresponding entry from `NODE_TABLE` in [`src/project/graph/graph.cpp`](https://github.com/HailToDodongo/pyrite64/blob/main/src/project/graph/graph.cpp). For distribution, consider creating a static library or contributing the nodes to the upstream `HailToDodongo/pyrite64` repository.