1
0
Fork
You've already forked asignals
0
A signal event bus for Luanti / Minetest
  • Lua 100%
Find a file
2026年04月09日 20:27:15 +10:00
init.lua tabs 2026年04月09日 19:03:01 +10:00
LICENSE Update LICENSE 2025年09月17日 12:32:52 +10:00
mod.conf Update mod.conf 2025年09月17日 13:55:05 +10:00
README.md better docs 2026年04月09日 20:27:15 +10:00

Event Bus

These functions are for signalling and listening for event triggers. Basically it's a way of your code being able to tell the whole game what just happened, such as if a player did some action, or some other event happened. Note that this is more for games than for general modding, but is a helpful API regardless.

This is a global set of functions that do what they say on the tin mostly. The exception is CONDITIONAL which is a bit more involved (see below). Signals and listeners not only allow you to sometimes save on constant checks by using one-off events instead, but you can also allow extending, avoid some dependencies, and generally make things more reactive and less constant polling. Instead of checking constantly whether the player has just died and then come back to life, we use core.register_on_respawnplayer. This is no different except it is generalised and you can signal whatever thing you want.

Example Use

LISTEN("on_player_damage", function(player, damage, source)
 core.chat_send_all(
 "Player "..player:get_player_name()..
 " was hit for "..damage.." HP"
 )
end)
LISTEN("can_player_take_damage", function(player, damage, source)
 if player:get_meta():get_string("invulnerable") == "true" then
 return false
 else
 return true
 end
end)
if CONDITIONAL("can_player_take_damage", player, damage, source) then
 SIGNAL("on_player_damage", player, damage, source)
end

You can also get type and autocomplete hints using the below:

---@param a number
---@param b string
---@diagnostic disable-next-line: duplicate-set-field
function SIGNAL.on_something_happen:signal(a, b) end -- doesn't actually change the function, just for hints/autocomplete
SIGNAL.on_something_happen:signal(a: number, b: string) -- now gives type hints

Extra Details

Everything below is only extra information and advice. It's not necessary for use of the API.

SIGNALS

-- Tell everything listening to `tag` that something happened.
SIGNAL(tag, ...)
-- Register a callback to be run when anything signals for this `tag`.
LISTEN(tag, function() end)

CONDITIONAL

If any signal returns ==false, it will return false immediately. Otherwise, if one or more callback returns true it returns true, and if nothing gets returned then it returns nil.

local mybool = CONDITIONAL(tag, ...)

Example:

LISTEN("can_be_damaged", function(self)
 if self._invulnerable then
 return false
 else
 return true
 end
end)
[...]
local is_damaged = CONDITIONAL("can_be_damaged", self)

This is good for conditions that need to be able to be extended, such as when an object should enter a "sleep" state, or to test if it is allowed to take some action.

Object Instances

You can get a standalone signal bus or singlular signal, by using the below. God knows why you'd do this, but sometimes it's helpful for autocomplete. These work exactly the same as the usual functions, but may be more explicit. The downside is that now it's tied to that object / mod etc and you have interdependence which is the one thing event buses are good at fixing.

-- Single event:
local on_event_happen = SIGNAL.Signal.new()
on_event_happen:signal()
on_event_happen:listen(function(...) end)
on_event_happen:conditional()
-- Whole event bus:
local signalbus_copy = SIGNAL.SignalBus.new()
signalbus_copy.SIGNAL("on_something_happen", 38.2, 4)

Things you should be aware of

Signals are not good for all things; generally you should use them only for things that have an outward effect. That means when you SIGNAL, that block of code that contains the SIGNAL call doesn't care what happens next. Only the LISTEN-er cares. This means signals are one-way. You tell the entire world something happened, and whether someone picks that up and does something is entirely up to circumstance, whether or not you currently know there is a listener out there who will pick it up! This is not like normal procedural code, where you cause the effects within the very block of code you're writing. That said, you could create an entire game using nothing but signals. It would just be confusing.

For the sake of consistency it's often best to observe the following rules with signals; signals where you don't care about the effect and you are only broadcasting the event happening at all, should be named on_[something happen]ed. The thing has already happened and it's on that thing happened. For CONDITIONALs, it would be can_[something happen], or is_[something true].

Examples of when it makes sense to use them are:

  • on_gamestate_changed --> when you change from "waiting for players" to "playing the game", maybe other parts of the game want to know this
  • on_mapgen_finished --> if you have a set area you're generating, often you want to know when it's done, and callbacks can be just thrown away and use signals instead
  • is_player_ready --> could be that you are waiting for players to be ready, and you don't care how "ready" is determined
  • on_player_ready --> same thing, but this time it's when it actually triggers actively; some code has determined the player to be ready and has told you so

In reality, what it will be useful for is up to your circumstances. If you know you need it, use it. Don't try to shove it into some mod or game that doesn't benefit from it.

Another thing to note is that, used in this way you are essentially polluting the global scope of signal names. If you feel the need to do something like on_mymod_thing_happened instead of on_thing_happened then you should seriously question whether you should be using signals for it. Signals are more or less the domain of the game they are within. If you make a mod that expects on_player_damaged to give you a player, damage number and an object, but then some other mod expects it to give an entity instead of an object, you're gonna have a bad time. That's why signals are intended to be very non-specific to the event, or be game-domain where it doesn't matter. Don't make mods that use signals in silly ways in other words. Now that you can extract single signals out and add them to your object piecemeal, you can make them specific to your code instead of global, but be aware that this almost invariably increases interdependence rather than reducing it. It basically removes all the benefits an event bus has.

Removing Signals

You may de-list a callback by having it return any, true. Both SIGNAL and CONDITIONAL will check for this second return value and then remove the callback from the list so it is ignored from then on. However, you absolutely should never rely on this since it means you're probably registering many listeners that potentially might not get removed. For example, never have an entity use LISTEN. Instead, use LISTEN after defining the entity and have it run on all your entities. Same with players; if you have players hook into callbacks on join, it might lead to a runaway situation in which there are thousands of useless callbacks.

Performance

This is fast enough that in non-abuse situations, you never really need to consider it. Particularly with LuaJIT. That said, it is not highly optimised either, so avoid using it for thousands of calls. If you need such a use case, you should roll your own custom solution, and you probably have an X Y problem anyway if you need to signal tens of thousands of times per server step.

See the documentation in the code itself more more advanced information, there are plenty of comments explaining how things are used. Many features have extensive hints so just mentioning SIGNAL is enough to get example uses.