Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
105 changes: 81 additions & 24 deletions src/mods/ScriptRunner.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -147,12 +147,16 @@ ScriptState::ScriptState(const ScriptState::GarbageCollectionData& gc_data,bool
re["msg"] = api::re::msg;
re["on_pre_application_entry"] = [this](const char* name, sol::function fn) { m_pre_application_entry_fns.emplace(utility::hash(name), fn); };
re["on_application_entry"] = [this](const char* name, sol::function fn) { m_application_entry_fns.emplace(utility::hash(name), fn); };
re["on_pre_gui_draw_element"] = [this](sol::function fn) { m_pre_gui_draw_element_fns.emplace_back(fn); };
re["on_gui_draw_element"] = [this](sol::function fn) { m_gui_draw_element_fns.emplace_back(fn); };
re["on_draw_ui"] = [this](sol::function fn) { m_on_draw_ui_fns.emplace_back(fn); };
re["on_frame"] = [this](sol::function fn) { m_on_frame_fns.emplace_back(fn); };
re["on_script_reset"] = [this](sol::function fn) { m_on_script_reset_fns.emplace_back(fn); };
re["on_config_save"] = [this](sol::function fn) { m_on_config_save_fns.emplace_back(fn); };
re["on_pre_gui_draw_element"] = [this](sol::function fn) { m_pre_gui_draw_element_fns.add(fn); };
re["on_gui_draw_element"] = [this](sol::function fn) { m_gui_draw_element_fns.add(fn); };
re["on_draw_ui"] = [this](sol::function fn) { m_on_draw_ui_fns.add(fn); };
re["on_frame"] = [this](sol::function fn) { m_on_frame_fns.add(fn); };
re["on_script_reset"] = [this](sol::function fn) { m_on_script_reset_fns.add(fn); };
re["on_config_save"] = [this](sol::function fn) { m_on_config_save_fns.add(fn); };
re.new_enum("CallbackNextAction",
"CONTINUE", ReCallbackNextAction::CONTINUE,
"STOP", ReCallbackNextAction::STOP
);
m_lua["re"] = re;

auto thread = m_lua.create_table();
Expand Down Expand Up @@ -455,12 +459,32 @@ sol::protected_function_result ScriptState::handle_protected_result(sol::protect
return result;
}

bool ScriptState::should_remove_hook(const sol::protected_function_result &result) {
if (!result.valid()) {
return false;
}

auto result_obj = result.get<sol::object>();

if (!result_obj.valid() || result_obj.is<sol::nil_t>()) {
return false;
}

auto action = result_obj.as<ReCallbackNextAction>();
return action == ReCallbackNextAction::STOP;
}

void ScriptState::on_frame() {
try {
std::scoped_lock _{ m_execution_mutex };

for (auto& fn : m_on_frame_fns) {
handle_protected_result(fn());
auto guard = m_on_frame_fns.acquire_iteration();
for (auto& fn : m_on_frame_fns.get()) {
auto result = handle_protected_result(fn());

if (should_remove_hook(result)) {
m_on_frame_fns.remove(fn);
}
}
} catch (const std::exception& e) {
ScriptRunner::get()->spew_error(e.what());
Expand All @@ -476,8 +500,13 @@ void ScriptState::on_draw_ui() {
try {
std::scoped_lock _{ m_execution_mutex };

for (auto& fn : m_on_draw_ui_fns) {
handle_protected_result(fn());
auto guard = m_on_draw_ui_fns.acquire_iteration();
for (auto& fn : m_on_draw_ui_fns.get()) {
auto result = handle_protected_result(fn());

if (should_remove_hook(result)) {
m_on_draw_ui_fns.remove(fn);
}
}
} catch (const std::exception& e) {
ScriptRunner::get()->spew_error(e.what());
Expand Down Expand Up @@ -584,9 +613,18 @@ bool ScriptState::on_pre_gui_draw_element(REComponent* gui_element, void* contex
try {
std::scoped_lock _{ m_execution_mutex };

for (auto& fn : m_pre_gui_draw_element_fns) {
if (sol::object result = handle_protected_result(fn(gui_element, context)); !result.is<sol::nil_t>() && result.is<bool>() && result.as<bool>() == false) {
any_false = true;
auto guard = m_pre_gui_draw_element_fns.acquire_iteration();
for (auto& fn : m_pre_gui_draw_element_fns.get()) {
if (auto result = handle_protected_result(fn(gui_element, context))) {
auto result_obj = result.get<sol::object>();

if (!result_obj.is<sol::nil_t>() && result_obj.is<bool>() && result_obj.as<bool>() == false) {
any_false = true;
} else {
if (should_remove_hook(result)) {
m_pre_gui_draw_element_fns.remove(fn);
}
}
}
}
} catch (const std::exception& e) {
Expand All @@ -602,8 +640,13 @@ void ScriptState::on_gui_draw_element(REComponent* gui_element, void* context) {
try {
std::scoped_lock _{ m_execution_mutex };

for (auto& fn : m_gui_draw_element_fns) {
handle_protected_result(fn(gui_element, context));
auto guard = m_gui_draw_element_fns.acquire_iteration();
for (auto& fn : m_gui_draw_element_fns.get()) {
auto result = handle_protected_result(fn(gui_element, context));

if (should_remove_hook(result)) {
m_gui_draw_element_fns.remove(fn);
}
}
} catch (const std::exception& e) {
ScriptRunner::get()->spew_error(e.what());
Expand All @@ -616,12 +659,20 @@ void ScriptState::on_script_reset() try {
std::scoped_lock _{ m_execution_mutex };

// We first call on_config_save functions so scripts can save prior to reset.
for (auto& fn : m_on_config_save_fns) {
handle_protected_result(fn());
auto guard_save = m_on_config_save_fns.acquire_iteration();
for (auto& fn : m_on_config_save_fns.get()) {
auto result = handle_protected_result(fn());
if (should_remove_hook(result)) {
m_on_config_save_fns.remove(fn);
}
}

for (auto& fn : m_on_script_reset_fns) {
handle_protected_result(fn());
auto guard_reset = m_on_script_reset_fns.acquire_iteration();
for (auto& fn : m_on_script_reset_fns.get()) {
auto result = handle_protected_result(fn());
if (should_remove_hook(result)) {
m_on_script_reset_fns.remove(fn);
}
}
} catch (const std::exception& e) {
ScriptRunner::get()->spew_error(e.what());
Expand All @@ -632,8 +683,13 @@ void ScriptState::on_script_reset() try {
void ScriptState::on_config_save() try {
std::scoped_lock _{ m_execution_mutex };

for (auto& fn : m_on_config_save_fns) {
handle_protected_result(fn());
auto guard = m_on_config_save_fns.acquire_iteration();
for (auto& fn : m_on_config_save_fns.get()) {
auto result = handle_protected_result(fn());

if (should_remove_hook(result)) {
m_on_config_save_fns.remove(fn);
}
}
}
catch (const std::exception& e) {
Expand Down Expand Up @@ -757,11 +813,11 @@ void ScriptState::add_delegate_callback(sdk::DelegateInvocation& invo, sol::prot
auto it = s_delegates.find(invo.object);

if (it != s_delegates.end()) {
it->second->callbacks.push_back(callback);
it->second->callbacks.add(callback);
} else {
auto storage = std::make_unique<DelegateStorage>();
storage->owner = shared_from_this();
storage->callbacks.push_back(callback);
storage->callbacks.add(callback);

static auto system_object_t = sdk::find_type_definition("System.Object");

Expand Down Expand Up @@ -795,7 +851,8 @@ void ScriptState::delegate_callback(sdk::VMContext* ctx, REManagedObject* obj) {

auto __ = owner_state->scoped_lock();

for (auto& fn : delegate->callbacks) {
auto guard = delegate->callbacks.acquire_iteration();
for (auto& fn : delegate->callbacks.get()) {
try {
auto script_result = fn(obj);

Expand Down
22 changes: 15 additions & 7 deletions src/mods/ScriptRunner.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@

#include "sdk/RETypeDB.hpp"
#include "utility/FunctionHook.hpp"
#include <utility/ScopeGuard.hpp>
#include <utility/SafeCallbackVector.hpp>

#include "Mod.hpp"

Expand Down Expand Up @@ -119,6 +121,11 @@ class ScriptState : public std::enable_shared_from_this<ScriptState> {
LAST
};

enum class ReCallbackNextAction: uint32_t {
CONTINUE = 0,
STOP = 1
};

struct GarbageCollectionData {
GarbageCollectionHandler gc_handler{GarbageCollectionHandler::REFRAMEWORK_MANAGED};
GarbageCollectionType gc_type{GarbageCollectionType::FULL};
Expand All @@ -134,6 +141,7 @@ class ScriptState : public std::enable_shared_from_this<ScriptState> {

void run_script(const std::string& p);
sol::protected_function_result handle_protected_result(sol::protected_function_result result); // because protected_functions don't throw
bool should_remove_hook(const sol::protected_function_result &result);

void on_frame();
void on_draw_ui();
Expand Down Expand Up @@ -283,12 +291,12 @@ class ScriptState : public std::enable_shared_from_this<ScriptState> {

std::unordered_map<RETransform*, sol::protected_function> m_on_update_transform_fns{};

std::vector<sol::protected_function> m_pre_gui_draw_element_fns{};
std::vector<sol::protected_function> m_gui_draw_element_fns{};
std::vector<sol::protected_function> m_on_draw_ui_fns{};
std::vector<sol::protected_function> m_on_frame_fns{};
std::vector<sol::protected_function> m_on_script_reset_fns{};
std::vector<sol::protected_function> m_on_config_save_fns{};
SafeCallbackVector<sol::protected_function> m_pre_gui_draw_element_fns{};
SafeCallbackVector<sol::protected_function> m_gui_draw_element_fns{};
SafeCallbackVector<sol::protected_function> m_on_draw_ui_fns{};
SafeCallbackVector<sol::protected_function> m_on_frame_fns{};
SafeCallbackVector<sol::protected_function> m_on_script_reset_fns{};
SafeCallbackVector<sol::protected_function> m_on_config_save_fns{};

struct HookDef {
::REManagedObject* obj{nullptr};
Expand All @@ -308,7 +316,7 @@ class ScriptState : public std::enable_shared_from_this<ScriptState> {

struct DelegateStorage {
std::weak_ptr<ScriptState> owner{}; // Weak pointer to the ScriptState that owns this delegate storage because the ScriptState may be deleted.
std::vector<sol::protected_function> callbacks{};
SafeCallbackVector<sol::protected_function> callbacks{};

};

Expand Down
81 changes: 81 additions & 0 deletions src/utility/SafeCallbackVector.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
#pragma once

#include <vector>
#include <utility/ScopeGuard.hpp>

// Safe callback vector that handles additions during iteration
// Prevents vector reallocation crashes by queuing new callbacks added during iteration
// and merging them when iteration completes.
template<typename T>
class SafeCallbackVector {
private:
std::vector<T> m_callbacks;
std::vector<T> m_pending;
std::vector<T> m_pending_removals;
int m_use_count = 0;

public:
// Create a scope guard that increments use count and merges pending when released
auto acquire_iteration() {
m_use_count++;
return utility::ScopeGuard([this]() {
m_use_count--;
if (m_use_count == 0) {
// Apply pending removals
if (!m_pending_removals.empty()) {
for (const auto& item : m_pending_removals) {
auto it = std::find(m_callbacks.begin(), m_callbacks.end(), item);
if (it != m_callbacks.end()) {
m_callbacks.erase(it);
}
}
m_pending_removals.clear();
}

// Merge pending additions
if (!m_pending.empty()) {
m_callbacks.insert(m_callbacks.end(), m_pending.begin(), m_pending.end());
m_pending.clear();
}
}
});
}

// Get the callback vector for iteration
std::vector<T>& get() {
return m_callbacks;
}

// Add a callback (queues if currently iterating)
void add(const T& callback) {
if (m_use_count > 0) {
m_pending.push_back(callback);
} else {
m_callbacks.push_back(callback);
}
}

// Remove a callback (queues if currently iterating)
void remove(const T& callback) {
if (m_use_count > 0) {
m_pending_removals.push_back(callback);
} else {
auto it = std::find(m_callbacks.begin(), m_callbacks.end(), callback);
if (it != m_callbacks.end()) {
m_callbacks.erase(it);
}
}
}

// Clear all callbacks
void clear() {
m_callbacks.clear();
m_pending.clear();
m_pending_removals.clear();
}

// Check if empty
bool empty() const {
return m_callbacks.empty() && m_pending.empty();
}
};