diff --git a/.github/workflows/ci_workflow.yml b/.github/workflows/ci_workflow.yml
new file mode 100644
index 0000000..fd3bb92
--- /dev/null
+++ b/.github/workflows/ci_workflow.yml
@@ -0,0 +1,26 @@
+name: Run tests
+
+on: [push]
+
+jobs:
+ build_and_run:
+ name: Build and run tests
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v2
+ with:
+ lfs: true
+ - uses: actions/setup-java@v1
+ with:
+ java-version: '17'
+
+ - name: Build && Run
+ run: |
+ deployer_url="https://raw.githubusercontent.com/Insality/defold-deployer/4/deployer.sh"
+ curl -s ${deployer_url} | bash -s lbd --headless --settings ./test/test.ini
+
+ - name: Upload coverage reports to Codecov
+ uses: codecov/codecov-action@v4.0.1
+ with:
+ token: ${{ secrets.CODECOV_TOKEN }}
+ slug: insality/defold-event
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index 027138d..1db3f96 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,3 +4,10 @@
Thumbs.db
manifest.private.der
manifest.public.der
+
+deployer_version_settings.txt
+
+.deployer_cache
+bob.jar
+deployer_build_stats.csv
+dist
\ No newline at end of file
diff --git a/.vscode/settings.json b/.vscode/settings.json
new file mode 100644
index 0000000..fa96091
--- /dev/null
+++ b/.vscode/settings.json
@@ -0,0 +1,11 @@
+{
+ "Lua.diagnostics.globals": [
+ "lua_script_instance",
+ "sys",
+ "init",
+ "describe",
+ "before",
+ "after",
+ "it"
+ ]
+}
diff --git a/README.md b/README.md
index 96f5320..ba0e455 100644
--- a/README.md
+++ b/README.md
@@ -1,8 +1,10 @@
![](media/logo.png)
-[![Github-sponsors](https://img.shields.io/badge/sponsor-30363D?style=for-the-badge&logo=GitHub-Sponsors&logoColor=#EA4AAA)](https://github.com/sponsors/insality) [![Ko-Fi](https://img.shields.io/badge/Ko--fi-F16061?style=for-the-badge&logo=ko-fi&logoColor=white)](https://ko-fi.com/insality) [![BuyMeACoffee](https://img.shields.io/badge/Buy%20Me%20a%20Coffee-ffdd00?style=for-the-badge&logo=buy-me-a-coffee&logoColor=black)](https://www.buymeacoffee.com/insality)
-
[![GitHub release (latest by date)](https://img.shields.io/github/v/tag/insality/defold-event?style=for-the-badge&label=Release)](https://github.com/Insality/defold-event/tags)
+[![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/insality/defold-event/ci-workflow.yml?branch=master&style=for-the-badge)](https://github.com/Insality/defold-event/actions)
+[![codecov](https://img.shields.io/codecov/c/github/Insality/defold-event?style=for-the-badge)](https://codecov.io/gh/Insality/defold-event)
+
+[![Github-sponsors](https://img.shields.io/badge/sponsor-30363D?style=for-the-badge&logo=GitHub-Sponsors&logoColor=#EA4AAA)](https://github.com/sponsors/insality) [![Ko-Fi](https://img.shields.io/badge/Ko--fi-F16061?style=for-the-badge&logo=ko-fi&logoColor=white)](https://ko-fi.com/insality) [![BuyMeACoffee](https://img.shields.io/badge/Buy%20Me%20a%20Coffee-ffdd00?style=for-the-badge&logo=buy-me-a-coffee&logoColor=black)](https://www.buymeacoffee.com/insality)
# Event
@@ -26,7 +28,7 @@
Open your `game.project` file and add the following line to the dependencies field under the project section:
-**[Event v2](https://github.com/Insality/defold-event/archive/refs/tags/2.zip)**
+**[Event v3](https://github.com/Insality/defold-event/archive/refs/tags/2.zip)**
```
https://github.com/Insality/defold-event/archive/refs/tags/2.zip
@@ -70,6 +72,7 @@ Memory allocation tracking is turned off in release builds, regardless of the `g
### Quick API Reference
```lua
+-- Event Module
event.set_logger(logger)
event.create(callback, [callback_context])
event:subscribe(callback, [callback_context])
@@ -78,10 +81,13 @@ event:is_subscribed(callback, [callback_context])
event:trigger(...)
event:is_empty()
event:clear()
+
+-- Global Events Module
events.subscribe(name, callback, [callback_context])
events.unsubscribe(name, callback, [callback_context])
events.is_subscribed(name, callback, [callback_context])
events.trigger(name, ...)
+events.is_empty(name)
events.clear(name)
events.clear_all()
```
@@ -228,6 +234,8 @@ Trigger the event, causing all subscribed callbacks to be executed.
- **Parameters:** Any number of parameters to be passed to the subscribed callbacks.
+- **Return Value:** The return value of the last callback executed.
+
- **Usage Example:**
```lua
@@ -351,6 +359,24 @@ Throw a global event with the specified name. All subscribed callbacks will be e
events.trigger("on_game_over", "arg1", "arg2")
```
+**events.is_empty**
+---
+```lua
+events.is_empty(name)
+```
+Check if the specified global event has no subscribed callbacks.
+
+- **Parameters:**
+ - `name`: The name of the global event to check.
+
+- **Return Value:** `true` if the global event has no subscribed callbacks, `false` otherwise.
+
+- **Usage Example:**
+
+```lua
+local is_empty = events.is_empty("on_game_over")
+```
+
**events.clear**
---
```lua
@@ -425,6 +451,15 @@ If you have any issues, questions or suggestions please [create an issue](https:
- The `event:subscribe` and `event:unsubscribe` now return boolean value of success
+### **V3**
+
+ Changelog
+
+ - Event Trigger now returns value of last executed callback
+ - Add `events.is_empty(name)` function
+ - Add tests for Event and Global Events modules
+
+
## ❤️ Support project ❤️
diff --git a/USE_CASES.md b/USE_CASES.md
index f06e029..74fca88 100644
--- a/USE_CASES.md
+++ b/USE_CASES.md
@@ -42,6 +42,7 @@ function final(self)
end
```
+
### 2. Component-specific Events
Design components with built-in events, enabling customizable behavior for instances of the component. This is particularly useful for UI elements like buttons where you want to bind specific actions to events like clicks.
@@ -85,3 +86,28 @@ function init(self)
end
```
+
+
+### 3. Lua annotations
+
+You can use annotations to document your events and make them easier to understand.
+
+```lua
+---This event is triggered when the sound button is clicked.
+---@class event.on_sound_click: event
+---@field trigger fun(_, is_sound_on: boolean): boolean|nil
+---@field subscribe fun(_, callback: fun(is_sound_on: boolean): boolean, _): boolean
+
+local event = require("event.event")
+
+---@type event.on_sound_click
+local on_sound_click = event.create()
+
+-- This callback params will be checked by Lua linter
+on_sound_click:subscribe(function(is_sound_on)
+ print("Sound is on: ", is_sound_on)
+end)
+
+-- Trigger params will be checked by Lua linter
+on_sound_click:trigger(true)
+```
diff --git a/event/annotations.lua b/event/annotations.lua
new file mode 100644
index 0000000..2b4b430
--- /dev/null
+++ b/event/annotations.lua
@@ -0,0 +1,24 @@
+---@class event
+---@field create fun(callback: function|nil, callback_context: any|nil): event
+---@field subscribe fun(self: event, callback: function, callback_context: any|nil): boolean
+---@field unsubscribe fun(self: event, callback: function, callback_context: any|nil): boolean
+---@field is_subscribed fun(self: event, callback: function, callback_context: any|nil): boolean
+---@field trigger fun(self: event, a: any, b: any, c: any, d: any, e: any, f: any, g: any, h: any, i: any, j: any): nil
+---@field clear fun(self: event): nil
+---@field is_empty fun(self: event): boolean
+
+---@class events
+---@field subscribe fun(event_name: string, callback: function, callback_context: any|nil): boolean
+---@field unsubscribe fun(event_name: string, callback: function, callback_context: any|nil): boolean
+---@field is_subscribed fun(event_name: string, callback: function, callback_context: any|nil): boolean
+---@field trigger fun(event_name: string, ...: any): any @Result of the last callback
+---@field clear fun(name: string): nil
+---@field clear_all fun(): nil
+---@field is_empty fun(name: string): boolean
+
+---@class event.logger
+---@field trace fun(logger: event.logger, message: string, data: any|nil)
+---@field debug fun(logger: event.logger, message: string, data: any|nil)
+---@field info fun(logger: event.logger, message: string, data: any|nil)
+---@field warn fun(logger: event.logger, message: string, data: any|nil)
+---@field error fun(logger: event.logger, message: string, data: any|nil)
diff --git a/event/event.lua b/event/event.lua
index 3f823d3..4798f1d 100644
--- a/event/event.lua
+++ b/event/event.lua
@@ -5,13 +5,14 @@ if not IS_DEBUG then
MEMORY_THRESHOLD_WARNING = 0
end
+
---@class event @Event Module
----@field callbacks table
local M = {}
--- Use empty function to save a bit of memory
local EMPTY_FUNCTION = function(_, message, context) end
+---@type event.logger
M.logger = {
trace = EMPTY_FUNCTION,
debug = EMPTY_FUNCTION,
@@ -21,7 +22,7 @@ M.logger = {
}
----@param logger_instance logger
+---@param logger_instance event.logger
function M.set_logger(logger_instance)
M.logger = logger_instance
end
@@ -31,12 +32,13 @@ end
---Create new event instance. If callback is passed, it will be subscribed to the event.
---@param callback function|nil
---@param callback_context any|nil
+---@return event
function M.create(callback, callback_context)
local instance = setmetatable({
_mapping = nil, -- Used for memory threshold warning, only in debug mode
callbacks = nil,
}, {
- __index = M
+ __index = M,
})
if callback then
@@ -123,25 +125,20 @@ function M:is_subscribed(callback, callback_context)
end
----Trigger the event. All subscribed callbacks will be called in the order they were subscribed.
----@param a any
----@param b any
----@param c any
----@param d any
----@param e any
----@param f any
----@param g any
----@param h any
----@param i any
----@param j any
local last_used_memory = 0
-function M:trigger(a, b, c, d, e, f, g, h, i, j)
+
+---Trigger the event. All subscribed callbacks will be called in the order they were subscribed.
+---@vararg any
+---@return any @Result of the last triggered callback
+function M:trigger(...)
if not self.callbacks then
return
end
local current_script_context = lua_script_instance.Get()
+ local result = nil
+
for index = 1, #self.callbacks do
local callback = self.callbacks[index]
@@ -153,11 +150,11 @@ function M:trigger(a, b, c, d, e, f, g, h, i, j)
last_used_memory = collectgarbage("count")
end
- local ok, errors
+ local ok, result_or_error
if callback.callback_context then
- ok, errors = pcall(callback.callback, callback.callback_context, a, b, c, d, e, f, g, h, i, j)
+ ok, result_or_error = pcall(callback.callback, callback.callback_context, ...)
else
- ok, errors = pcall(callback.callback, a, b, c, d, e, f, g, h, i, j)
+ ok, result_or_error = pcall(callback.callback, ...)
end
if current_script_context ~= callback.script_context then
@@ -166,9 +163,12 @@ function M:trigger(a, b, c, d, e, f, g, h, i, j)
if not ok then
local traceback = debug.traceback()
- M.logger:error("An error occurred during event processing", { errors = errors, traceback = traceback })
+ M.logger:error("An error occurred during event processing", { errors = result_or_error, traceback = traceback })
-- Print again cause it's just better to see it in the console
- print(traceback)
+ pprint(result_or_error)
+ pprint(traceback)
+ else
+ result = result_or_error
end
if MEMORY_THRESHOLD_WARNING > 0 then
@@ -182,6 +182,8 @@ function M:trigger(a, b, c, d, e, f, g, h, i, j)
end
end
end
+
+ return result
end
diff --git a/event/events.lua b/event/events.lua
index d2ac0d2..1bb3f78 100644
--- a/event/events.lua
+++ b/event/events.lua
@@ -9,6 +9,7 @@ M.events = {}
---Throws the event
---@param event_name string Event name
---@vararg any @Event params
+---@return any @Result of the last triggered callback
function M.trigger(event_name, ...)
local event = M.events[event_name]
@@ -18,7 +19,7 @@ function M.trigger(event_name, ...)
})
if event then
- event:trigger(...)
+ return event:trigger(...)
end
end
@@ -87,4 +88,16 @@ function M.is_subscribed(event_name, callback, callback_context)
end
+---Check if the event is empty
+---@param event_name string Event name
+---@return boolean @True if event is empty. If event is not exist, return true
+function M.is_empty(event_name)
+ if not M.events[event_name] then
+ return true
+ end
+
+ return M.events[event_name]:is_empty()
+end
+
+
return M
diff --git a/game.project b/game.project
index 7e842e4..e8b0aaf 100644
--- a/game.project
+++ b/game.project
@@ -1,5 +1,5 @@
[bootstrap]
-main_collection = /example/example.collectionc
+main_collection = /test/test.collectionc
[script]
shared_state = 1
@@ -13,10 +13,11 @@ input_method = HiddenInputField
[project]
title = Defold Event
-version = 1
+version = 3
publisher = Insality
developer = Maksim Tuprikov, Insality
dependencies#0 = https://github.com/Insality/defold-log/archive/refs/tags/2.zip
+dependencies#1 = https://github.com/britzl/deftest/archive/refs/tags/2.8.0.zip
[library]
include_dirs = event
diff --git a/settings_deployer b/settings_deployer
new file mode 100644
index 0000000..1ca6a3f
--- /dev/null
+++ b/settings_deployer
@@ -0,0 +1,14 @@
+# Path to bob folder. It will find and save new bob.jar files inside
+bob_folder=./
+
+# You can point bob version for project in format "filename:sha"
+bob_sha="181:fd1ad4c17bfdcd890ea7176f2672c35102384419"
+
+# Select Defold channel. Values: stable, beta, alpha
+bob_channel="stable"
+
+# If true, it will check and download latest bob version. It will ignore bob_sha param
+use_latest_bob=false
+
+# Select Defold build server
+build_server="https://build.defold.com"
\ No newline at end of file
diff --git a/test/test.collection b/test/test.collection
new file mode 100644
index 0000000..fe2bf3f
--- /dev/null
+++ b/test/test.collection
@@ -0,0 +1,39 @@
+name: "default"
+scale_along_z: 0
+embedded_instances {
+ id: "test"
+ data: "components {\n"
+ " id: \"test\"\n"
+ " component: \"/test/test.gui\"\n"
+ " position {\n"
+ " x: 0.0\n"
+ " y: 0.0\n"
+ " z: 0.0\n"
+ " }\n"
+ " rotation {\n"
+ " x: 0.0\n"
+ " y: 0.0\n"
+ " z: 0.0\n"
+ " w: 1.0\n"
+ " }\n"
+ " property_decls {\n"
+ " }\n"
+ "}\n"
+ ""
+ position {
+ x: 0.0
+ y: 0.0
+ z: 0.0
+ }
+ rotation {
+ x: 0.0
+ y: 0.0
+ z: 0.0
+ w: 1.0
+ }
+ scale3 {
+ x: 1.0
+ y: 1.0
+ z: 1.0
+ }
+}
diff --git a/test/test.gui b/test/test.gui
new file mode 100644
index 0000000..d0f87d1
--- /dev/null
+++ b/test/test.gui
@@ -0,0 +1,10 @@
+script: "/test/test.gui_script"
+background_color {
+ x: 0.0
+ y: 0.0
+ z: 0.0
+ w: 0.0
+}
+material: "/builtins/materials/gui.material"
+adjust_reference: ADJUST_REFERENCE_PARENT
+max_nodes: 512
diff --git a/test/test.gui_script b/test/test.gui_script
new file mode 100644
index 0000000..d671d8b
--- /dev/null
+++ b/test/test.gui_script
@@ -0,0 +1,9 @@
+local deftest = require("deftest.deftest")
+
+function init(self)
+ deftest.add(require("test.test_event"))
+ deftest.add(require("test.test_events"))
+
+ local is_report = sys.get_config("test.report") == "1"
+ deftest.run({ coverage = { enabled = is_report } })
+end
diff --git a/test/test.ini b/test/test.ini
new file mode 100644
index 0000000..eeec6a9
--- /dev/null
+++ b/test/test.ini
@@ -0,0 +1,9 @@
+[bootstrap]
+main_collection = /test/test.collectionc
+
+[display]
+height = 256
+width = 256
+
+[test]
+report = 1
\ No newline at end of file
diff --git a/test/test_event.lua b/test/test_event.lua
new file mode 100644
index 0000000..b6134f1
--- /dev/null
+++ b/test/test_event.lua
@@ -0,0 +1,128 @@
+local event = require("event.event")
+
+return function()
+ describe("Defold Event", function()
+ it("Instantiate Event", function()
+ local test_event = event.create()
+ assert(test_event)
+ end)
+
+ it("Subscribe and Unsubscribe", function()
+ local test_event = event.create()
+ local f = function() end
+
+ test_event:subscribe(f)
+ assert(#test_event.callbacks == 1)
+
+ test_event:unsubscribe(f)
+ assert(#test_event.callbacks == 0)
+ end)
+
+ it("Trigger", function()
+ local test_event = event.create()
+ local counter = 0
+ local f = function() counter = counter + 1 end
+
+ test_event:subscribe(f)
+ test_event:trigger()
+ assert(counter == 1)
+ test_event:trigger()
+ assert(counter == 2)
+
+ test_event:unsubscribe(f)
+ test_event:trigger()
+ assert(counter == 2)
+ end)
+
+ it("Trigger with params", function()
+ local test_event = event.create()
+ local counter = 0
+ local f = function(a, b) counter = counter + a + b end
+
+ test_event:subscribe(f)
+ test_event:trigger(1, 2)
+ assert(counter == 3)
+ end)
+
+ it("One function can be subscribed only once", function()
+ local test_event = event.create()
+ local counter = 0
+ local f = function() counter = counter + 1 end
+
+ local is_subscribed = test_event:subscribe(f)
+ assert(is_subscribed == true)
+
+ is_subscribed = test_event:subscribe(f)
+ assert(is_subscribed == false)
+ assert(#test_event.callbacks == 1)
+ end)
+
+ it("Event is_subscribed", function()
+ local test_event = event.create()
+ local f = function() end
+
+ assert(test_event:is_subscribed(f) == false)
+
+ test_event:subscribe(f)
+ assert(test_event:is_subscribed(f) == true)
+ end)
+
+ it("Subscribe with context", function()
+ local test_event = event.create()
+ local last_context
+ local f = function(context) last_context = context end
+
+ test_event:subscribe(f, "context")
+ test_event:trigger("foo", "bar")
+ assert(last_context == "context")
+ end)
+
+ it("Event is_empty", function()
+ local test_event = event.create()
+ local f = function() end
+
+ assert(test_event:is_empty() == true)
+
+ test_event:subscribe(f)
+ assert(test_event:is_empty() == false)
+ end)
+
+ it("Event clear", function()
+ local test_event = event.create()
+ local f = function() end
+
+ test_event:subscribe(f)
+ assert(test_event:is_empty() == false)
+
+ test_event:clear()
+ assert(test_event:is_empty() == true)
+ end)
+
+ it("Event trigger returns result", function()
+ local test_event = event.create()
+ local f = function() return "result" end
+
+ test_event:subscribe(f)
+ local result = test_event:trigger()
+ assert(result == "result")
+ end)
+
+ it("Event trigger returns last subscriber result", function()
+ local test_event = event.create()
+ local f1 = function() return "result1" end
+ local f2 = function() return "result2" end
+
+ test_event:subscribe(f1)
+ test_event:subscribe(f2)
+
+ local result = test_event:trigger()
+ assert(result == "result2")
+ end)
+
+ it("Event trigger returns nil if no subscribers", function()
+ local test_event = event.create()
+ local result = test_event:trigger()
+ assert(result == nil)
+ end)
+ end)
+end
diff --git a/test/test_events.lua b/test/test_events.lua
new file mode 100644
index 0000000..2e74ff6
--- /dev/null
+++ b/test/test_events.lua
@@ -0,0 +1,125 @@
+local events = require("event.events")
+
+return function()
+ describe("Defold Events", function()
+ before(function()
+ events.clear_all()
+ end)
+
+ it("Events Subscribe and Unsubscribe", function()
+ local f = function() end
+
+ events.subscribe("test", f)
+ assert(events.is_subscribed("test", f) == true)
+
+ events.unsubscribe("test", f)
+ assert(events.is_subscribed("test", f) == false)
+ end)
+
+ it("Events Trigger", function()
+ local counter = 0
+ local f = function() counter = counter + 1 end
+
+ events.subscribe("test", f)
+ events.trigger("test")
+ assert(counter == 1)
+ events.trigger("test")
+ assert(counter == 2)
+
+ events.unsubscribe("test", f)
+ events.trigger("test")
+ assert(counter == 2)
+ end)
+
+ it("Events Trigger with params", function()
+ local counter = 0
+ local f = function(a, b) counter = counter + a + b end
+
+ events.subscribe("test", f)
+ events.trigger("test", 1, 2)
+ assert(counter == 3)
+ end)
+
+ it("One function can be subscribed only once", function()
+ local counter = 0
+ local f = function() counter = counter + 1 end
+
+ local is_subscribed = events.subscribe("test", f)
+ assert(is_subscribed == true)
+
+ is_subscribed = events.subscribe("test", f)
+ assert(is_subscribed == false)
+ assert(events.is_subscribed("test", f) == true)
+ end)
+
+ it("Events Clear", function()
+ local counter = 0
+ local f = function() counter = counter + 1 end
+
+ events.subscribe("test", f)
+ events.trigger("test")
+ assert(counter == 1)
+
+ events.clear("test")
+ events.trigger("test")
+ assert(counter == 1)
+ end)
+
+ it("Events Clear All", function()
+ local counter = 0
+ local f = function() counter = counter + 1 end
+
+ events.subscribe("test", f)
+ events.trigger("test")
+ assert(counter == 1)
+
+ events.clear_all()
+ events.trigger("test")
+ assert(counter == 1)
+ end)
+
+ it("Events is_subscribed", function()
+ local f = function() end
+
+ assert(events.is_subscribed("test", f) == false)
+
+ events.subscribe("test", f)
+ assert(events.is_subscribed("test", f) == true)
+ end)
+
+ it("Events is_empty", function()
+ assert(events.is_empty("test") == true)
+
+ local f = function() end
+ events.subscribe("test", f)
+ assert(events.is_empty("test") == false)
+ end)
+
+ it("Events Subscribe with context", function()
+ local last_context
+ local f = function(context) last_context = context end
+
+ events.subscribe("test", f, "context")
+ events.trigger("test", "foo", "bar")
+ assert(last_context == "context")
+ end)
+
+ it("Events Trigger returns result", function()
+ local f = function() return "result" end
+
+ events.subscribe("test", f)
+ local result = events.trigger("test")
+ assert(result == "result")
+ end)
+
+ it("Events Trigger return last callback result", function()
+ local f1 = function() return "result1" end
+ local f2 = function() return "result2" end
+
+ events.subscribe("test", f1)
+ events.subscribe("test", f2)
+ local result = events.trigger("test")
+ assert(result == "result2")
+ end)
+ end)
+end