Program Listing for File AgentFunctionDescription.cpp

Return to documentation for file (src/flamegpu/model/AgentFunctionDescription.cpp)

#include <nvrtc.h>
#include <cuda.h>
#include <fstream>
#include <iostream>
#include <sstream>
#include <string>
#include <regex>
#include <utility>
#include <vector>
#include <memory>
#include <set>

#include "flamegpu/model/AgentFunctionDescription.h"
#include "flamegpu/detail/cxxname.hpp"

namespace flamegpu {

CAgentFunctionDescription::CAgentFunctionDescription(std::shared_ptr<AgentFunctionData> data)
    : function(std::move(data)) { }
CAgentFunctionDescription::CAgentFunctionDescription(std::shared_ptr<const AgentFunctionData> data)
    : function(std::move(std::const_pointer_cast<AgentFunctionData>(data))) { }

bool CAgentFunctionDescription::operator==(const CAgentFunctionDescription& rhs) const {
    return *this->function == *rhs.function;  // Compare content is functionally the same
}
bool CAgentFunctionDescription::operator!=(const CAgentFunctionDescription& rhs) const {
    return !(*this == rhs);
}


std::string CAgentFunctionDescription::getName() const {
    return function->name;
}
std::string CAgentFunctionDescription::getInitialState() const {
    return function->initial_state;
}
std::string CAgentFunctionDescription::getEndState() const {
    return function->end_state;
}
MessageBruteForce::CDescription CAgentFunctionDescription::getMessageInput() const {
    if (auto m = function->message_input.lock())
        return MessageBruteForce::CDescription(m);
    THROW exception::OutOfBoundsException("Message input has not been set, "
        "in AgentFunctionDescription::getMessageInput().");
}
MessageBruteForce::CDescription CAgentFunctionDescription::getMessageOutput() const {
    if (auto m = function->message_output.lock())
        return MessageBruteForce::CDescription(m);
    THROW exception::OutOfBoundsException("Message output has not been set, "
        "in AgentFunctionDescription::getMessageOutput().");
}
bool CAgentFunctionDescription::getMessageOutputOptional() const {
    return this->function->message_output_optional;
}
CAgentDescription CAgentFunctionDescription::getAgentOutput() const {
    if (auto a = function->agent_output.lock())
        return CAgentDescription(a);
    THROW exception::OutOfBoundsException("Agent output has not been set, "
        "in AgentFunctionDescription::getAgentOutput().");
}
std::string CAgentFunctionDescription::getAgentOutputState() const {
    if (auto a = function->agent_output.lock())
        return function->agent_output_state;
    THROW exception::OutOfBoundsException("Agent output has not been set, "
        "in AgentFunctionDescription::getAgentOutputState().");
}
bool CAgentFunctionDescription::getAllowAgentDeath() const {
    return function->has_agent_death;
}
bool CAgentFunctionDescription::hasMessageInput() const {
    return function->message_input.lock() != nullptr;
}
bool CAgentFunctionDescription::hasMessageOutput() const {
    return function->message_output.lock() != nullptr;
}
bool CAgentFunctionDescription::hasAgentOutput() const {
    return function->agent_output.lock() != nullptr;
}
bool CAgentFunctionDescription::hasFunctionCondition() const {
    return function->condition != nullptr;
}
AgentFunctionWrapper* CAgentFunctionDescription::getFunctionPtr() const {
    return function->func;
}
AgentFunctionConditionWrapper* CAgentFunctionDescription::getConditionPtr() const {
    return function->condition;
}
bool CAgentFunctionDescription::isRTC() const {
    return !function->rtc_source.empty();
}

AgentFunctionDescription::AgentFunctionDescription(std::shared_ptr<AgentFunctionData> data)
    : CAgentFunctionDescription(std::move(data)) { }

void AgentFunctionDescription::setInitialState(const std::string &init_state) {
    if (auto p = function->parent.lock()) {
        if (p->states.find(init_state) != p->states.end()) {
            // Check if this agent function is already in a layer
            auto mdl = function->model.lock();
            if (!mdl) {
                THROW exception::ExpiredWeakPtr();
            }
            for (const auto &l : mdl->layers) {
                for (const auto &f : l->agent_functions) {
                    // Agent fn is in layer
                    if (f->name == this->function->name) {
                        // search all functions in that layer
                        for (const auto &f2 : l->agent_functions) {
                            if (const auto &a2 = f2->parent.lock()) {
                                if (const auto &a1 = this->function->parent.lock()) {
                                    // Same agent
                                    if (a2->name == a1->name) {
                                        // Skip ourself
                                        if (f2->name == this->function->name)
                                            continue;
                                        if (f2->initial_state == init_state ||
                                            f2->end_state == init_state) {
                                            THROW exception::InvalidAgentFunc("Agent functions's '%s' and '%s', within the same layer "
                                                "cannot share any input or output states, this is not permitted, "
                                                "in AgentFunctionDescription::setInitialState().",
                                                f2->name.c_str(), this->function->name.c_str());
                                        }
                                    }
                                }
                            }
                        }
                    }
                }
            }
            // Checks passed, make change
            this->function->initial_state = init_state;
        } else {
            THROW exception::InvalidStateName("Agent ('%s') does not contain state '%s', "
                "in AgentFunctionDescription::setInitialState().",
                p->name.c_str(), init_state.c_str());
        }
    } else {
        THROW exception::InvalidParent("Agent parent has expired, "
            "in AgentFunctionDescription::setInitialState().");
    }
}
void AgentFunctionDescription::setEndState(const std::string &exit_state) {
    if (auto p = function->parent.lock()) {
        if (p->states.find(exit_state) != p->states.end()) {
            // Check if this agent function is already in a layer
            auto mdl = function->model.lock();
            if (!mdl) {
                THROW exception::ExpiredWeakPtr();
            }
            for (const auto &l : mdl->layers) {
                for (const auto &f : l->agent_functions) {
                    // Agent fn is in layer
                    if (f->name == this->function->name) {
                        // search all functions in that layer
                        for (const auto &f2 : l->agent_functions) {
                            if (const auto &a2 = f2->parent.lock()) {
                                if (const auto &a1 = this->function->parent.lock()) {
                                    // Same agent
                                    if (a2->name == a1->name) {
                                        // Skip ourself
                                        if (f2->name == this->function->name)
                                            continue;
                                        if (f2->initial_state == exit_state ||
                                            f2->end_state == exit_state) {
                                            THROW exception::InvalidAgentFunc("Agent functions's '%s' and '%s', within the same layer "
                                                "cannot share any input or output states, this is not permitted, "
                                                "in AgentFunctionDescription::setEndState().",
                                                f2->name.c_str(), this->function->name.c_str());
                                        }
                                    }
                                }
                            }
                        }
                    }
                }
            }
            // Checks passed, make change
            this->function->end_state = exit_state;
        } else {
            THROW exception::InvalidStateName("Agent ('%s') does not contain state '%s', "
                "in AgentFunctionDescription::setEndState().",
                p->name.c_str(), exit_state.c_str());
        }
    } else {
        THROW exception::InvalidParent("Agent parent has expired, "
            "in AgentFunctionDescription::setEndState().");
    }
}
void AgentFunctionDescription::setMessageInput(const std::string &message_name) {
    if (auto other = function->message_output.lock()) {
        if (message_name == other->name) {
            THROW exception::InvalidMessageName("Message '%s' is already bound as message output in agent function %s, "
                "the same message cannot be input and output by the same function, "
                "in AgentFunctionDescription::setMessageInput().",
                message_name.c_str(), function->name.c_str());
        }
    }
    auto mdl = function->model.lock();
    if (!mdl) {
        THROW exception::ExpiredWeakPtr();
    }
    auto a = mdl->messages.find(message_name);
    if (a != mdl->messages.end()) {
        // Just compare the classname is the same, to allow for the various approaches to namespace use. This should only be required for RTC functions.
        auto message_in_classname = detail::cxxname::getUnqualifiedName(this->function->message_in_type);
        auto demangledClassName = detail::cxxname::getUnqualifiedName(detail::curve::CurveRTCHost::demangle(a->second->getType()));
        if (message_in_classname == demangledClassName) {
            this->function->message_input = a->second;
        } else {
            THROW exception::InvalidMessageType("Message ('%s') type '%s' does not match type '%s' applied to FLAMEGPU_AGENT_FUNCTION ('%s'), "
                "in AgentFunctionDescription::setMessageInput().",
                message_name.c_str(), demangledClassName.c_str(), message_in_classname.c_str(), this->function->name.c_str());
        }
    } else {
        THROW exception::InvalidMessageName("Model ('%s') does not contain message '%s', "
            "in AgentFunctionDescription::setMessageInput().",
            mdl->name.c_str(), message_name.c_str());
    }
}
void AgentFunctionDescription::setMessageInput(MessageBruteForce::CDescription message) {
    if (message.message->model.lock() != function->model.lock()) {
        THROW exception::DifferentModel("Attempted to use agent description from a different model, "
            "in AgentFunctionDescription::setAgentOutput().");
    }
    if (auto other = function->message_output.lock()) {
        if (message.getName() == other->name) {
            THROW exception::InvalidMessageName("Message '%s' is already bound as message output in agent function %s, "
                "the same message cannot be input and output by the same function, "
                "in AgentFunctionDescription::setMessageInput().",
                message.getName().c_str(), function->name.c_str());
        }
    }
    auto mdl = function->model.lock();
    if (!mdl) {
        THROW exception::ExpiredWeakPtr();
    }
    auto a = mdl->messages.find(message.getName());
    if (a != mdl->messages.end()) {
        if (a->second == message.message) {
            // Just compare the classname is the same, to allow for the various approaches to namespace use. This should only be required for RTC functions.
            auto message_in_classname = detail::cxxname::getUnqualifiedName(this->function->message_in_type);
            auto demangledClassName = detail::cxxname::getUnqualifiedName(detail::curve::CurveRTCHost::demangle(a->second->getType()));
            if (message_in_classname == demangledClassName) {
                this->function->message_input = a->second;
            } else {
                THROW exception::InvalidMessageType("Message ('%s') type '%s' does not match type '%s' applied to FLAMEGPU_AGENT_FUNCTION ('%s'), "
                    "in AgentFunctionDescription::setMessageInput().",
                    a->second->name.c_str(), demangledClassName.c_str(), message_in_classname.c_str(), this->function->name.c_str());
            }
        } else {
            THROW exception::InvalidMessage("Message '%s' is not from Model '%s', "
                "in AgentFunctionDescription::setMessageInput().",
                message.getName().c_str(), mdl->name.c_str());
        }
    } else {
        THROW exception::InvalidMessageName("Model ('%s') does not contain message '%s', "
            "in AgentFunctionDescription::setMessageInput().",
            mdl->name.c_str(), message.getName().c_str());
    }
}
void AgentFunctionDescription::setMessageOutput(const std::string &message_name) {
    if (auto other = function->message_input.lock()) {
        if (message_name == other->name) {
            THROW exception::InvalidMessageName("Message '%s' is already bound as message output in agent function %s, "
                "the same message cannot be input and output by the same function, "
                "in AgentFunctionDescription::setMessageOutput().",
                message_name.c_str(), function->name.c_str());
        }
    }
    // Clear old value
    if (this->function->message_output_optional) {
        if (auto b = this->function->message_output.lock()) {
            b->optional_outputs--;
        }
    }
    auto mdl = function->model.lock();
    if (!mdl) {
        THROW exception::ExpiredWeakPtr();
    }
    auto a = mdl->messages.find(message_name);
    if (a != mdl->messages.end()) {
        // Just compare the classname is the same, to allow for the various approaches to namespace use. This should only be required for RTC functions.
        auto message_out_classname = detail::cxxname::getUnqualifiedName(this->function->message_out_type);
        auto demangledClassName = detail::cxxname::getUnqualifiedName(detail::curve::CurveRTCHost::demangle(a->second->getType()));
        if (message_out_classname == demangledClassName) {
            this->function->message_output = a->second;
            if (this->function->message_output_optional) {
                a->second->optional_outputs++;
            }
        } else {
            THROW exception::InvalidMessageType("Message ('%s') type '%s' does not match type '%s' applied to FLAMEGPU_AGENT_FUNCTION ('%s'), "
                "in AgentFunctionDescription::setMessageOutput().",
                message_name.c_str(), demangledClassName.c_str(), message_out_classname.c_str(), this->function->name.c_str());
        }
    } else {
        THROW exception::InvalidMessageName("Model ('%s') does not contain message '%s', "
            "in AgentFunctionDescription::setMessageOutput().",
            mdl->name.c_str(), message_name.c_str());
    }
}
void AgentFunctionDescription::setMessageOutput(MessageBruteForce::CDescription message) {
    if (message.message->model.lock() != function->model.lock()) {
        THROW exception::DifferentModel("Attempted to use agent description from a different model, "
            "in AgentFunctionDescription::setAgentOutput().");
    }
    if (auto other = function->message_input.lock()) {
        if (message.getName() == other->name) {
            THROW exception::InvalidMessageName("Message '%s' is already bound as message input in agent function %s, "
                "the same message cannot be input and output by the same function, "
                "in AgentFunctionDescription::setMessageOutput().",
                message.getName().c_str(), function->name.c_str());
        }
    }
    // Clear old value
    if (this->function->message_output_optional) {
        if (auto b = this->function->message_output.lock()) {
            b->optional_outputs--;
        }
    }
    auto mdl = function->model.lock();
    if (!mdl) {
        THROW exception::ExpiredWeakPtr();
    }
    auto a = mdl->messages.find(message.getName());
    if (a != mdl->messages.end()) {
        if (a->second == message.message) {
            // Just compare the classname is the same, to allow for the various approaches to namespace use. This should only be required for RTC functions.
            auto message_out_classname = detail::cxxname::getUnqualifiedName(this->function->message_out_type);
            auto demangledClassName = detail::cxxname::getUnqualifiedName(detail::curve::CurveRTCHost::demangle(a->second->getType()));
            if (message_out_classname == demangledClassName) {
                this->function->message_output = a->second;
                if (this->function->message_output_optional) {
                    a->second->optional_outputs++;
                }
            } else {
                THROW exception::InvalidMessageType("Message ('%s') type '%s' does not match type '%s' applied to FLAMEGPU_AGENT_FUNCTION ('%s'), "
                    "in AgentFunctionDescription::setMessageOutput().",
                    a->second->name.c_str(), demangledClassName.c_str(), message_out_classname.c_str(), this->function->name.c_str());
            }
        } else {
            THROW exception::InvalidMessage("Message '%s' is not from Model '%s', "
                "in AgentFunctionDescription::setMessageOutput().",
                message.getName().c_str(), mdl->name.c_str());
        }
    } else {
        THROW exception::InvalidMessageName("Model ('%s') does not contain message '%s', "
            "in AgentFunctionDescription::setMessageOutput()\n",
            mdl->name.c_str(), message.getName().c_str());
    }
}
void AgentFunctionDescription::setMessageOutputOptional(const bool output_is_optional) {
    if (output_is_optional != this->function->message_output_optional) {
        this->function->message_output_optional = output_is_optional;
        if (auto b = this->function->message_output.lock()) {
            if (output_is_optional)
                b->optional_outputs++;
            else
                b->optional_outputs--;
        }
    }
}
void AgentFunctionDescription::setAgentOutput(const std::string &agent_name, const std::string state) {
    // Set new
    auto mdl = function->model.lock();
    if (!mdl) {
        THROW exception::ExpiredWeakPtr();
    }
    auto a = mdl->agents.find(agent_name);
    if (a != mdl->agents.end()) {
        // Check agent state is valid
        if (a->second->states.find(state)!= a->second->states.end()) {    // Clear old value
            if (auto b = this->function->agent_output.lock()) {
                b->agent_outputs--;
            }
            this->function->agent_output = a->second;
            this->function->agent_output_state = state;
            a->second->agent_outputs++;  // Mark inside agent that we are using it as an output
        } else {
            THROW exception::InvalidStateName("Agent ('%s') does not contain state '%s', "
                "in AgentFunctionDescription::setAgentOutput().",
                agent_name.c_str(), state.c_str());
        }
    } else {
        THROW exception::InvalidAgentName("Model ('%s') does not contain agent '%s', "
            "in AgentFunctionDescription::setAgentOutput().",
            mdl->name.c_str(), agent_name.c_str());
    }
}
void AgentFunctionDescription::setAgentOutput(AgentDescription &agent, const std::string state) {
    if (agent.agent->model.lock() != function->model.lock()) {
        THROW exception::DifferentModel("Attempted to use agent description from a different model, "
            "in AgentFunctionDescription::setAgentOutput().");
    }
    // Set new
    auto mdl = function->model.lock();
    if (!mdl) {
        THROW exception::ExpiredWeakPtr();
    }
    auto a = mdl->agents.find(agent.getName());
    if (a != mdl->agents.end()) {
        if (*a->second == agent) {
            // Check agent state is valid
            if (a->second->states.find(state) != a->second->states.end()) {
                // Clear old value
                if (auto b = this->function->agent_output.lock()) {
                    b->agent_outputs--;
                }
                this->function->agent_output = a->second;
                this->function->agent_output_state = state;
                a->second->agent_outputs++;  // Mark inside agent that we are using it as an output
            } else {
                THROW exception::InvalidStateName("Agent ('%s') does not contain state '%s', "
                    "in AgentFunctionDescription::setAgentOutput().",
                    agent.getName().c_str(), state.c_str());
            }
        } else {
            THROW exception::InvalidMessage("Agent '%s' is not from Model '%s', "
                "in AgentFunctionDescription::setAgentOutput().",
                agent.getName().c_str(), mdl->name.c_str());
        }
    } else {
        THROW exception::InvalidMessageName("Model ('%s') does not contain agent '%s', "
            "in AgentFunctionDescription::setAgentOutput().",
            mdl->name.c_str(), agent.getName().c_str());
    }
}
void AgentFunctionDescription::setAllowAgentDeath(const bool has_death) {
    function->has_agent_death = has_death;
}

void AgentFunctionDescription::setRTCFunctionCondition(std::string func_cond_src) {
    // Use Regex to get agent function name
    std::regex rgx(R"###(.*FLAMEGPU_AGENT_FUNCTION_CONDITION\([ \t]*(\w+)[ \t]*)###");
    std::smatch match;
    std::string func_cond_name;
    if (std::regex_search(func_cond_src, match, rgx)) {
        if (match.size() == 2) {
            func_cond_name = match[1];
            // set the runtime agent function condition source in agent function data
            function->rtc_func_condition_name = func_cond_name;
            function->rtc_condition_source = func_cond_src;
            // TODO: Does this need emplacing in CUDAAgent?
        } else {
            THROW exception::InvalidAgentFunc("Runtime agent function condition is missing FLAMEGPU_AGENT_FUNCTION_CONDITION arguments e.g. 'FLAMEGPU_AGENT_FUNCTION_CONDITION(func_name)', "
                "in AgentDescription::setRTCFunctionCondition().");
        }
    } else {
        THROW exception::InvalidAgentFunc("Runtime agent function('%s') is missing FLAMEGPU_AGENT_FUNCTION_CONDITION, "
            "in AgentDescription::setRTCFunctionCondition().");
    }

    // append jitify program string and include
    std::string func_cond_src_str = std::string(func_cond_name + "_program\n");
#ifdef FLAMEGPU_OUTPUT_RTC_DYNAMIC_FILES
    func_cond_src_str.append("#line 1 \"").append(function->rtc_func_name).append("_impl_condition.cu\"\n");
#endif
    func_cond_src_str.append("#include \"flamegpu/runtime/DeviceAPI.cuh\"\n");
    // Append line pragma to correct file/line number in same format as FLAMEGPU_OUTPUT_RTC_DYNAMIC_FILES
#ifndef FLAMEGPU_OUTPUT_RTC_DYNAMIC_FILES
    func_cond_src_str.append("#line 1 \"").append(function->rtc_func_name).append("_impl_condition.cu\"\n");
#endif
    // If src begins (\r)\n, trim that
    // Append the function source
    if (func_cond_src.find_first_of("\n") <= 1) {
        func_cond_src_str.append(func_cond_src.substr(func_cond_src.find_first_of("\n") + 1));
    } else {
        func_cond_src_str.append(func_cond_src);
    }

    // update the agent function data
    function->rtc_func_condition_name = func_cond_name;
    function->rtc_condition_source = func_cond_src_str;
}
void AgentFunctionDescription::setRTCFunctionConditionFile(const std::string& file_path) {
    // Load file and forward to regular RTC method
    std::ifstream file;
    file.open(file_path);
    if (file.is_open()) {
        std::stringstream sstream;
        sstream << file.rdbuf();
        const std::string func_src = sstream.str();
        setRTCFunctionCondition(func_src);
    }
    THROW exception::InvalidFilePath("Unable able to open file '%s', "
        "in AgentDescription::newRTCFunctionFile().",
        file_path.c_str());
}

MessageBruteForce::Description AgentFunctionDescription::MessageInput() {
    if (auto m = function->message_input.lock())
        return MessageBruteForce::Description(m);
    THROW exception::OutOfBoundsException("Message input has not been set, "
        "in AgentFunctionDescription::MessageInput().");
}
MessageBruteForce::Description AgentFunctionDescription::MessageOutput() {
    if (auto m = function->message_output.lock())
        return MessageBruteForce::Description(m);
    THROW exception::OutOfBoundsException("Message output has not been set, "
        "in AgentFunctionDescription::MessageOutput().");
}
bool &AgentFunctionDescription::MessageOutputOptional() {
    return function->message_output_optional;
}
bool &AgentFunctionDescription::AllowAgentDeath() {
    return function->has_agent_death;
}
AgentDescription AgentFunctionDescription::AgentOutput() {
    if (auto a = function->agent_output.lock())
        return AgentDescription(a);
    THROW exception::OutOfBoundsException("Agent output has not been set, "
        "in AgentFunctionDescription::AgentOutput().");
}

AgentFunctionDescription AgentDescription::newRTCFunction(const std::string& function_name, const std::string& func_src) {
    if (agent->functions.find(function_name) == agent->functions.end()) {
        // Use Regex to get agent function name, and input/output message type
        std::regex rgx(R"###(.*FLAMEGPU_AGENT_FUNCTION\([ \t]*(\w+),[ \t]*([:\w]+),[ \t]*([:\w]+)[ \t]*\))###");
        std::smatch match;
        if (std::regex_search(func_src, match, rgx)) {
            if (match.size() == 4) {
                std::string code_func_name = match[1];  // not yet clear if this is required
                std::string in_type_name = match[2];
                std::string out_type_name = match[3];
                if (in_type_name == "flamegpu::MessageSpatial3D" || in_type_name == "flamegpu::MessageSpatial2D" || out_type_name == "flamegpu::MessageSpatial3D" || out_type_name == "flamegpu::MessageSpatial2D") {
                    if (agent->variables.find("_auto_sort_bin_index") == agent->variables.end()) {
                        agent->variables.emplace("_auto_sort_bin_index", Variable(1, std::vector<unsigned int> {0}));
                    }
                }
                // set the runtime agent function source in agent function data
                std::string func_src_str = std::string(function_name + "_program\n");
#ifdef FLAMEGPU_OUTPUT_RTC_DYNAMIC_FILES
                func_src_str.append("#line 1 \"").append(code_func_name).append("_impl.cu\"\n");
#endif
                func_src_str.append("#include \"flamegpu/runtime/DeviceAPI.cuh\"\n");
                // Include the required headers for the input message type.
                std::string in_type_include_name = in_type_name.substr(in_type_name.find_last_of("::") + 1);
                func_src_str = func_src_str.append("#include \"flamegpu/runtime/messaging/"+ in_type_include_name + "/" + in_type_include_name + "Device.cuh\"\n");
                // If the message input and output types do not match, also include the input type
                if (in_type_name != out_type_name) {
                    std::string out_type_include_name = out_type_name.substr(out_type_name.find_last_of("::") + 1);
                    func_src_str = func_src_str.append("#include \"flamegpu/runtime/messaging/"+ out_type_include_name + "/" + out_type_include_name + "Device.cuh\"\n");
                }
                // Append line pragma to correct file/line number in same format as FLAMEGPU_OUTPUT_RTC_DYNAMIC_FILES
#ifndef FLAMEGPU_OUTPUT_RTC_DYNAMIC_FILES
                func_src_str.append("#line 1 \"").append(code_func_name).append("_impl.cu\"\n");
#endif
                // If src begins (\r)\n, trim that
                // Append the function source
                if (func_src.find_first_of("\n") <= 1) {
                    func_src_str.append(func_src.substr(func_src.find_first_of("\n") + 1));
                } else {
                    func_src_str.append(func_src);
                }
                auto rtn = std::shared_ptr<AgentFunctionData>(new AgentFunctionData(this->agent->shared_from_this(), function_name, func_src_str, in_type_name, out_type_name, code_func_name));
                agent->functions.emplace(function_name, rtn);
                return AgentFunctionDescription(rtn);
            } else {
                THROW exception::InvalidAgentFunc("Runtime agent function('%s') is missing FLAMEGPU_AGENT_FUNCTION arguments e.g. (func_name, message_input_type, message_output_type), "
                    "in AgentDescription::newRTCFunction().",
                    agent->name.c_str());
            }
        } else {
            THROW exception::InvalidAgentFunc("Runtime agent function('%s') is missing FLAMEGPU_AGENT_FUNCTION, "
                "in AgentDescription::newRTCFunction().",
                agent->name.c_str());
        }
    }
    THROW exception::InvalidAgentFunc("Agent ('%s') already contains function '%s', "
        "in AgentDescription::newRTCFunction().",
        agent->name.c_str(), function_name.c_str());
}

AgentFunctionDescription AgentDescription::newRTCFunctionFile(const std::string& function_name, const std::string& file_path) {
    if (agent->functions.find(function_name) == agent->functions.end()) {
        // Load file and forward to regular RTC method
        std::ifstream file;
        file.open(file_path);
        if (file.is_open()) {
            std::stringstream sstream;
            sstream << file.rdbuf();
            const std::string func_src = sstream.str();
            return newRTCFunction(function_name, func_src);
        }
        THROW exception::InvalidFilePath("Unable able to open file '%s', "
            "in AgentDescription::newRTCFunctionFile().",
            file_path.c_str());
    }
    THROW exception::InvalidAgentFunc("Agent ('%s') already contains function '%s', "
        "in AgentDescription::newRTCFunctionFile().",
        agent->name.c_str(), function_name.c_str());
}

}  // namespace flamegpu