Guide to signals

From BeeStation Wiki
Revision as of 06:18, 28 December 2023 by Itsmeowdev (talk | contribs) (Add patterns and clarify send_signal)
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)
Jump to navigation Jump to search

What is a signal?

A signal is a standard interface for calling procs on components and other objects. It prevents holding hard references and allows the procs to be called on multiple objects from one "event". It is an event-subscriber system, where one thing "shouts", or emits an event, and multiple things "listen".

Why signals?

In code, signals can improve quality and stability, because you do not have to collect a list of things and then call a proc on them if you want to inform them of a certain event happening. For example, previously, you may have needed to hold a reference to something or maintain a list of things that want to hear about an event occuring. Signals are built into every datum and object, and perform this duty automatically. Signals can also replace the need to check values on a component or other object, as you can simply emit a signal requesting it, and have the other object listen for it and respond.

For example, many things in the game care if a player dies. Because of that, we have a signal called COMSIG_MOB_DEATH, that is emitted by mobs when they die. It provides data with it as well (what emitted it, current health, etc.) that may be useful to the "listener". Signals can also be returned with a value to the emitter, allowing further interactions between objects.

How do I use signals?

Signals are made of several parts.

An emitter, which calls SEND_SIGNAL(). This emits the signal to anything that is currently listening to the emitter.

A listener, which calls RegisterSignal with the emitter as an argument, and provides a signal type and a signal callback.

A signal type, which is the type of signal being emitted or listened to. This is what COMSIG_MOB_DEATH is - it's a unique string that denotes the type of signal being sent, and when it is sent or how it is to be used.

A signal callback, which is a proc that receives data from the emitter's signal for a specific signal type.

Adding or selecting a type

All signals have a type, which is the unique string describing what the signal is used for or when it is emitted. All types are stored in code/__DEFINES/dcs/signals/, and sorted into categories based on the type of the emitter. For example, any signal sent only by /mob/ types should be in signals_mob/signals_mob.dm.

This is what a signal type definition looks like. The string in the signal should be unique to the signal type - and is usually the lowercase of everything after COMSIG_. It is also standard to document when a signal is sent within the comment, either by stating it or providing a proc path for the case where SEND_SIGNAL is used.

#define COMSIG_MOB_DEATH "mob_death" //! from base of mob/death(): (gibbed)

The parent path for the signal should be the highest path for which the signal is relevant. For example, there is both COMSIG_MOB_DEATH and COMSIG_LIVING_DEATH, because some places only care when a living mob dies. When creating a signal, determine which context is most useful or applicable, and apply it to the highest object possible. COMSIG_LIVING_DEATH provides additional arguments (or context) when the signal is emitted, making it necessary should anything require this information.

Listening for a signal

This is one of the most common patterns in the entire codebase, so it is absolutely vital to understand. When you want to receive a signal from an atom or datum, you call RegisterSignal with that atom or datum as the first argument, a signal type, and then a signal listener.

This is often done during Initialize(), however it can take place at any time so long as the emitter exists. Do note that RegisterSignal is a type on /datum, so it is implicitly calling src.RegisterSignal(). This means you can technically force another datum to register a signal listener via other_thing.RegisterSignal(emitter), however this is not recommended.

RegisterSignal(emitter_here, COMSIG_MOB_DEATH, PROC_REF(on_mob_death))

As you can see, the signal registration requires a signal listener. These are procs with a special format. In a signal's comment, or when it is emitted, there is usually a list of arguments, or simply no arguments. These arguments are provided to the signal listener as additional data. When defining a listener, you can choose to include these arguments if you need them.

/datum/example/proc/on_mob_death(mob/parent, gibbed)
    SIGNAL_HANDLER

As you can see, the first argument provided to a signal listener is ALWAYS the emitter, so you can assume for COMSIG_MOB_DEATH that it is of the type /mob. The remaining arguments are defined by the emitter itself when the signal is sent. You do not need to include any arguments if you do not want them.

You can also see that signal listeners always start with SIGNAL_HANDLER. This is a special DEFINE that disables sleeping within the proc, as sleeping in a signal listener can break the master controller's timing. It is important to include this in all signal listeners, since it is not checked automatically otherwise.

Stopping listening to a signal

Registering a signal on an emitter stores a reference to that emitter. This is problematic if the emitter were to be deleted or dissociated from the listener, as there is not a reference the other way around. It is generally the responsibility of the listener to control its own references to the emitter. For example, if we have an object that was listening for another object's death, but only once, we could unregister the signal during on_mob_death so as to prevent holding a reference.

Unregistering a signal is simple:

UnregisterSignal(emitter_here, COMSIG_MOB_DEATH)

This will remove src's listener for COMSIG_MOB_DEATH.

It is also important to know that all atoms and datums will automatically unregister all their signals when they are destroyed, so there is no need to do so yourself during Destroy(), however some things like components or species need to remove signals when they are 'dissociated', such as on_species_loss, or when a component is detached, as they are not always immediately destroyed.

Emitting or sending a signal

To send a signal, and retrieve results from listeners, you simply use the SEND_SIGNAL define.

SEND_SIGNAL(src, COMSIG_MOB_DEATH)

The first argument is the emitter, so often it is src, however this is not a rule. You can also emit signals from' other objects! It is a common pattern to emit a signal from another object, and have the object listen to signals on itself to respond to it. The second argument is always the type. You may provide additional arguments that will be sent as extra arguments to all listeners, as discussed earlier.

Another key feature of SEND_SIGNAL is that it can return a result. If a signal listener has a return value, it will be given as the result of SEND_SIGNAL. This means you can retrieve and act on data from listeners.

For example, this is used by emag signals:

/atom/proc/use_emag(mob/user, obj/item/card/emag/hacker)
	if(!SEND_SIGNAL(src, COMSIG_ATOM_SHOULD_EMAG, user))
		SEND_SIGNAL(src, COMSIG_ATOM_ON_EMAG, user, hacker)

If the result of COMSIG_ATOM_SHOULD_EMAG is FALSE, then COMSIG_ATOM_ON_EMAG is sent as well. Do note that this is intentionally inverted in case of an error, since procs will return null when they error, so this should be taken into consideration when sending signals to unknown listeners.

Signal Patterns

There are many ways to utilize signals, here are some common ones:

  • Event->Listener: Use a global signal to emit an 'event' occurring to anything that is listening.
  • Component<->Parent: Use a signal to communicate data between a component and its parent. This avoids the bad practice of GetComponent(), and allows components to react to events from the parent. This can use combinations of other patterns, but with components rather than objects.
  • Object->Object: Use a signal to inform an object of something happening to it. The listening object will register a signal to itself, if it cares about this interaction.
  • QDELETING: COMSIG_PARENT_QDELETING is one of the most used signals, it fires when the emitter is Destroy()ing. This allows the listener to clean up any references it has to the emitter, so it can delete properly.
  • Object->Listener: Use a signal to inform listeners of something happening within an object. This is used to avoid hard-coding unnecessary references. An example of this is the MOB_LOGOUT or MOB_DEATH signal.
  • Self->Listener: Use a signal to react to an event from within an object inside that same object. This is used when the two events are not related, to avoid hard-coding the event response. It can also be used to react to events from within the parent type without performing a method override.


Global Signals

Global signals are a special type of signal that can be emitted to listeners at any time, and can be listened to by any object. They do not require specifying where to send a signal to. This is helpful for events that may impact a large variety of objects in varying ways, or when the relationship between two events is weak but unnecessary, making hard-coding it bad practice. A good example of this is the explosion global signal. Many things care if an explosion has happened, such as the Tachyon-Doppler array, but it would not make sense to hard-code that logic into the explode proc. Hence, a global signal.

To emit a global signal, use SEND_GLOBAL_SIGNAL:

SEND_GLOBAL_SIGNAL(COMSIG_GLOB_CREW_MANIFEST_UPDATE)

Listening to a global signal requires listening to SSdcs, which is the subsystem responsible for signal emissions. This is because behind the scenes, SEND_GLOBAL_SIGNAL actually just sends a signal to SSdcs.

RegisterSignal(SSdcs, COMSIG_GLOB_CREW_MANIFEST_UPDATE, PROC_REF(on_crew_manifest_update))
Contribution guides
General Development, Downloading the source code / hosting a server, Guide to git, Game resources category, Guide to changelogs
Database (MySQL) Setting up the database, MySQL
Coding Understanding SS13 code, SS13 for experienced programmers, Binary flags‎, Text Formatting, Guide to signals
Mapping Guide to mapping, Map merger, Exploration Ruins
Spriting Guide to spriting
Wiki Guide to contributing to the wiki, Wikicode