You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
@@ -6,76 +6,136 @@ As an alternative to this system, Geode introduces **events**. Events are essent
6
6
7
7
Events are primarily interacted with through three classes: [`Event`](/classes/geode/Event), [`EventListener`](/classes/geode/EventListener), and [`EventFilter`](/classes/geode/EventFilter). `Event` is the base class for the events that are being broadcast; `EventListener` listens for events, and it uses an `EventFilter` to decide which events to listen to. Let's explore how they work through **an example**.
8
8
9
+
## Creating events
10
+
9
11
Consider a system where one mod introduces a **drag-and-drop API** to Geode, so the user can just drag files over the GD window. Now, this mod itself probably won't know how to handle every single file type ever; instead, it exposes an API so other mods can handle actually dealing with different file types. However, using a delegate-based system here would be quite undesirable - there is definitely more than one file type in existence. While the mod could just have a list of delegates instead of a single one, those delegates have to manually deal with telling the mod to remove themselves from the list when they want to stop listening for events.
10
12
11
13
Instead, the drag-and-drop API should leveradge the Geode event system by first defining a new type of event based on the `Event` base class:
> :warning: Note that we have structured the event this way (with protected variables and getters) so that it is **read-only**.
40
+
32
41
Now, the drag-and-drop mod can post new events by simple creating a `DragDropEvent` and calling `post` on it.
33
42
34
43
```cpp
35
-
void handleFilesDroppedOnWindow(...) {
36
-
...
44
+
// Assume those variables actually have useful values
45
+
std::vector<std::filesystem::path> files;
46
+
CCPoint location = CCPoint { 0.0f, 0.0f };
47
+
48
+
DragDropEvent(files, location).post();
37
49
38
-
DragDropEvent(...).post();
39
-
}
40
50
```
41
51
42
52
That's all - the drag-and-drop mod can now rest assured that any mod expecting drag-and-drop events has received them.
43
53
44
-
Let's see how that listening part goes, through an example mod that expects [GDShare](https://github.com/hjfod/GDShare-mod) level files and imports them:
54
+
## Listening to events
55
+
56
+
Listening to events is done using an `EventListener`. An event listener needs an `EventFilter`, so that it knows what events to listen to. This default EventFilter will pass a **pointer** to the event as the parameter to the callback we have to define. Here is a very simple example that listens to events for the **entire runtime of the game** - a **global listener**, as you might call it.
// Execute runs the code inside **when your mod is loaded**
69
+
$execute {
70
+
// This technically doesn't leak memory, since the listener should live for the entirety of the program
71
+
new EventListener<EventFilter<DragDropEvent>>(+[](DragDropEvent* ev) {
72
+
for (std::filesystem::path& file : ev->getFiles()) {
73
+
log::debug("File dropped: {}", file);
74
+
75
+
// ... handle the files here
76
+
}
77
+
78
+
// We have to propagate the event further, so that other listeners
79
+
// can handle this event
80
+
return ListenerResult::Propagate;
81
+
});
82
+
}
83
+
```
84
+
85
+
Notice that our callback returns a `ListenerResult`, more specifically `ListenerResult::Propagate`. This tells the event system that this specific event should **propagate** to the next listeners that are expecting this type of event. If you wish to **stop** this propagation from happening (let's say you don't want **.gmd** files to be propagated to other listeners), then you can return `ListenerResult::Stop`.
new EventListener<EventFilter<DragDropEvent>>(+[](DragDropEvent* ev) {
51
-
for (auto& file : ev->getFiles()) {
99
+
for (std::filesystem::path& file : ev->getFiles()) {
100
+
log::debug("File dropped: {}", file);
52
101
if (file.extension() == ".gmd") {
53
-
handleGMDImport(file);
102
+
log::info("Detected .gmd file: {}", file);
54
103
55
-
// This stops event propagation, marking the file as handled.
56
-
// Stopping propagation is usually not needed, and shouldn't be
57
-
// done by default, however sometimes you want to stop other
58
-
// listeners from dealing with the event - such as here, where
59
-
// importing the same file twice would be undesirable
104
+
// Stop event propagation after this listener.
60
105
return ListenerResult::Stop;
61
106
}
62
107
}
63
-
// Propagate this event down the chain; aka, let other listeners see
64
-
// the event
108
+
109
+
//If no .gmd file was detected, propagate the event further
65
110
return ListenerResult::Propagate;
66
111
});
67
112
}
68
113
```
69
114
70
115
This is all the mod needs to do to set up a **global listener** - one that exists for the entire duration of the mod. Now, whenever a `DragDropEvent` is posted, the mod catches it and can do whatever it wants with it.
71
116
72
-
This code also uses the default templated `EventFilter` class, which just checks if an event is right type and then calls the callback if that is the case, stopping propagation if the callback requests it to do so. However, we sometimes also want to create custom filters.
117
+
This code also uses the default templated `EventFilter` class, which just checks if an event is right type and then calls the callback if that is the case, stopping propagation if the callback requests it to do so. However, we sometimes also want to create **custom filters**.
118
+
119
+
## Creating custom filters
73
120
74
-
For example, let's say another mod wants to use the drag-and-drop API, but instead of always listening for events, it wants to have a specific node in the UI that the user should drop files over. In this case, the listener should only exist while the node exists, and only accept events if it's over the node. We could of course deal with this in the global callback, however we can simplify our code by creating a custom filter that handles accepting the event. We can also include the file types to listen for in the filter itself, simplifying our code even further.
121
+
For example, let's say another mod wants to use the drag-and-drop API, but instead of always listening for events, it wants to have a **specific node in the UI that the user should drop files over**. In this case, the listener should only exist **while the node exists**, and only accept events if it's over the node. We could of course deal with this in the global callback, however we can simplify our code by creating a custom filter that handles accepting the event. We can also include the file types to listen for in the filter itself, simplifying our code even further.
75
122
76
123
We can create a custom `EventFilter` by inheriting from it:
> :warning: You have a lot of freedom when defining the EventFilter callback. Remember that the default `EventFilter<Event>` is of type `std::function<ListenerResult(Event*)>`, but your callback can look differently.
164
+
97
165
For the implementation of `handle`, we need to check that the event occurred on top of the target node:
When our `DragDropNode` is destroyed, the EventListener is automatically destroyed and unregistered aswell, so you don't need to do anything else.
138
232
139
233
However, using a member function is not always possible. For example, if you're hooking a class, [event listeners don't work in fields](/tutorials/fields.md#note-about-addresses); or if you want to listen for events on an existing node whose class you don't control.
140
234
141
235
In these cases, there exists a Geode-specific helper called [`CCNode::addEventListener`](/classes/cocos2d/CCNode#addEventListener). You can use this to **add event listeners to any node** - including existing ones by GD!
142
236
237
+
> :warning: Any `EventFilter` that is used in `addEventListener`**must** have their first **constructor param** as a `CCNode*`. The callback **lambda** should be the first argument passed to `addEventListener`, then you have to pass the next **constructor arguments** for your `EventFilter`
`addEventListener` is meant only for events that are have a target node - it assumes that the first parameter of the filter's constructor takes `this` as the argument. Other parameters to the filter's constructor, such as the file types here, can be passed as the rest of the argument list to `addEventListener`.
154
-
155
251
Any event listener added with `addEventListener` is automatically destroyed aswell when the node is destroyed. You can also provide a string ID for the event listener as the first argument to `addEventListener`, and then manually remove the listener later using [`removeEventListener`](/classes/cocos2d/CCNode#removeEventListener).
0 commit comments