Skip to content

Conversation

@CrosRoad95
Copy link
Contributor

@CrosRoad95 CrosRoad95 commented Nov 9, 2025

Motivation: You may have "spammy events" and you don't want to enable system that trigger them when user don't want it to save performance, for example position changed events, collision events

Summary by CodeRabbit

Release Notes

  • New Features
    • Added ability to remove event listeners through the scripting interface.
    • Implemented event lifecycle callbacks triggered when events transition between active and inactive states.

@Segfaultd
Copy link
Member

@coderabbitai review pls

@coderabbitai
Copy link

coderabbitai bot commented Nov 11, 2025

@Segfaultd: I'll review the changes in this pull request for you.

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai
Copy link

coderabbitai bot commented Nov 11, 2025

Walkthrough

The changes add event listener removal functionality to the scripting framework. A new Remove method is introduced to the Event class and exposed to Lua, backed by a RemoveEventListener implementation in the scripting engine. Event registration lifecycle callbacks track when the first listener registers and when the last listener unregisters.

Changes

Cohort / File(s) Change Summary
Event removal interface
code/framework/src/scripting/builtins/event.h
Added static Remove method to handle listener removal; exposed to Lua via cls["remove"] binding
Event listener management
code/framework/src/scripting/engine.h
Added EventRegistrationCallback type alias, RemoveEventListener method, and lifecycle callbacks (_onFirstEventRegisteredProc, _onLastEventUnregisteredProc); updated ListenEvent signature to use const reference for name parameter

Sequence Diagram(s)

sequenceDiagram
    participant Lua
    participant Event
    participant Engine
    
    Lua->>Event: on(name, fnc)
    Event->>Engine: ListenEvent(name, fnc)
    activate Engine
    Engine->>Engine: Check if first listener
    alt First listener
        Engine->>Engine: Trigger _onFirstEventRegisteredProc
    end
    Engine->>Engine: Add listener to map
    deactivate Engine
    
    Lua->>Event: remove(name, fnc)
    Event->>Engine: RemoveEventListener(name, fnc)
    activate Engine
    Engine->>Engine: Remove listener from map
    Engine->>Engine: Check if last listener
    alt Last listener removed
        Engine->>Engine: Trigger _onLastEventUnregisteredProc
        Engine->>Engine: Remove event key
    end
    deactivate Engine
Loading

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

  • The changes are focused and cohesive, introducing listener removal mechanics with clear intent
  • Modifications consist of new method declarations, type aliases, and callback additions without complex logic
  • The ListenEvent signature change is minimal (const reference parameter)

Poem

Hop, hop, the events now dance and play,
Register listeners, or sweep them away,
With Remove in hand, the rabbit takes control,
First and last callbacks play their role,
✨ Event management, refined and clean! 🐰

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title directly reflects the main changes: adding callbacks for first event registration and last event unregistration, which are the primary features introduced in the pull request.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a431029 and 62634a2.

📒 Files selected for processing (2)
  • code/framework/src/scripting/builtins/event.h (2 hunks)
  • code/framework/src/scripting/engine.h (4 hunks)
🔇 Additional comments (7)
code/framework/src/scripting/builtins/event.h (2)

13-15: LGTM! Clean implementation.

The Remove method follows the same pattern as the existing On method and appropriately delegates to the engine.


25-25: LGTM! Proper Lua exposure.

The binding provides a clean API for removing event listeners from Lua scripts.

code/framework/src/scripting/engine.h (5)

26-26: LGTM! Clean type alias.

The callback signature is appropriate for event lifecycle notifications.


40-41: LGTM! Consistent with existing callback pattern.

The callback members follow the same initialization pattern as other callbacks in the class.


153-159: LGTM! Consistent setter implementation.

The setter methods follow the established pattern in the class and properly use std::move for efficiency.


86-88: Consider lifecycle callbacks for remote events.

The new lifecycle callbacks only apply to local events (_eventHandlers), but not to remote events (_eventRemoteHandlers). While this may be intentional based on the use case, it's worth confirming whether remote events should also support first-registration/last-unregistration callbacks for consistency.

Should ListenRemoteEvent also track first registration and last unregistration? If so, a RemoveRemoteEventListener method would also be needed.


72-76: No changes required—sol::function comparison works as intended.

The sol::function::operator== uses the reference comparator by calling Lua's equality (lua_compare with LUA_OPEQ) on the two referenced values, following Lua's equality semantics (function identity in Lua). This means the code correctly removes the listener by identity, as intended.

Comment on lines +54 to +63
void ListenEvent(const std::string &name, sol::function fnc) {
auto &handlers = _eventHandlers[name];
bool firstRegistration = handlers.empty();

handlers.push_back(std::move(fnc));

if (firstRegistration && _onFirstEventRegisteredProc) {
_onFirstEventRegisteredProc(name);
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Critical: Data race on _eventHandlers.

ListenEvent modifies _eventHandlers without holding _executionMutex, but InvokeEvent (line 94) locks the mutex before iterating over the same map. This creates a data race where concurrent listener registration and event invocation can corrupt the map or cause undefined behavior.

Apply this diff to add mutex protection:

 void ListenEvent(const std::string &name, sol::function fnc) {
+    std::lock_guard<std::mutex> lock(_executionMutex);
+    
     auto &handlers         = _eventHandlers[name];
     bool firstRegistration = handlers.empty();

     handlers.push_back(std::move(fnc));

     if (firstRegistration && _onFirstEventRegisteredProc) {
         _onFirstEventRegisteredProc(name);
     }
 }

Note: If the callback _onFirstEventRegisteredProc might call back into the engine or take locks, consider invoking it after releasing the mutex to avoid potential deadlocks.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
void ListenEvent(const std::string &name, sol::function fnc) {
auto &handlers = _eventHandlers[name];
bool firstRegistration = handlers.empty();
handlers.push_back(std::move(fnc));
if (firstRegistration && _onFirstEventRegisteredProc) {
_onFirstEventRegisteredProc(name);
}
}
void ListenEvent(const std::string &name, sol::function fnc) {
std::lock_guard<std::mutex> lock(_executionMutex);
auto &handlers = _eventHandlers[name];
bool firstRegistration = handlers.empty();
handlers.push_back(std::move(fnc));
if (firstRegistration && _onFirstEventRegisteredProc) {
_onFirstEventRegisteredProc(name);
}
}
🤖 Prompt for AI Agents
In code/framework/src/scripting/engine.h around lines 54 to 63, ListenEvent
mutates the shared _eventHandlers map without acquiring _executionMutex while
InvokeEvent locks that same mutex, causing a data race; fix by acquiring the
mutex (e.g., std::scoped_lock or std::lock_guard on _executionMutex) around the
access that reads/creates the handlers and pushes the callback, record whether
this was the first registration while still holding the lock, then release the
lock before calling _onFirstEventRegisteredProc(name) (to avoid potential
deadlocks if that callback re-enters the engine); ensure the handler is
moved/added while locked and only invoke external callbacks after unlocking.

Comment on lines +65 to 84
void RemoveEventListener(const std::string &name, const sol::function &fnc) {
auto it = _eventHandlers.find(name);
if (it == _eventHandlers.end())
return;

auto &handlers = it->second;

handlers.erase(std::remove_if(handlers.begin(), handlers.end(),
[&](const sol::function &f) {
return f == fnc;
}),
handlers.end());

if (handlers.empty()) {
_eventHandlers.erase(it);
if (_onLastEventUnregisteredProc) {
_onLastEventUnregisteredProc(name);
}
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Critical: Data race on _eventHandlers.

RemoveEventListener modifies _eventHandlers without mutex protection, creating the same data race issue as ListenEvent. Concurrent removal and event invocation can lead to undefined behavior.

Apply this diff to add mutex protection:

 void RemoveEventListener(const std::string &name, const sol::function &fnc) {
+    std::lock_guard<std::mutex> lock(_executionMutex);
+    
     auto it = _eventHandlers.find(name);
     if (it == _eventHandlers.end())
         return;

     auto &handlers = it->second;

     handlers.erase(std::remove_if(handlers.begin(), handlers.end(),
                        [&](const sol::function &f) {
                            return f == fnc;
                        }),
         handlers.end());

     if (handlers.empty()) {
         _eventHandlers.erase(it);
         if (_onLastEventUnregisteredProc) {
             _onLastEventUnregisteredProc(name);
         }
     }
 }

Note: Similar to ListenEvent, consider whether _onLastEventUnregisteredProc should be invoked outside the lock if it might interact with the engine.

🤖 Prompt for AI Agents
In code/framework/src/scripting/engine.h around lines 65 to 84,
RemoveEventListener mutates _eventHandlers without synchronization causing a
data race; protect the function with the same mutex used by ListenEvent (acquire
a std::lock_guard or equivalent at the start of the function) while finding,
modifying and potentially erasing the handlers entry, and only call
_onLastEventUnregisteredProc after releasing the lock (or document/decide if it
must be invoked inside the lock) to avoid deadlocks or reentrancy issues.

@MafiaHub MafiaHub deleted a comment from zpl-zak Nov 12, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants