diff --git a/code/__DEFINES/components.dm b/code/__DEFINES/components.dm index 3625aa76472..5b8671338fc 100644 --- a/code/__DEFINES/components.dm +++ b/code/__DEFINES/components.dm @@ -1,8 +1,16 @@ +/// Used to trigger signals and call procs registered for that signal +/// The datum hosting the signal is automaticaly added as the first argument +/// Returns a bitfield gathered from all registered procs +/// Arguments given here are packaged in a list and given to _SendSignal #define SEND_SIGNAL(target, sigtype, arguments...) ( !target.comp_lookup || !target.comp_lookup[sigtype] ? NONE : target._SendSignal(sigtype, list(target, ##arguments)) ) #define SEND_GLOBAL_SIGNAL(sigtype, arguments...) ( SEND_SIGNAL(SSdcs, sigtype, ##arguments) ) +/// Return this from `/datum/component/Initialize` or `datum/component/OnTransfer` to have the component be deleted if it's applied to an incorrect type. +/// `parent` must not be modified if this is to be returned. +/// This will be noted in the runtime logs #define COMPONENT_INCOMPATIBLE 1 +/// Returned in PostTransfer to prevent transfer, similar to `COMPONENT_INCOMPATIBLE` #define COMPONENT_NOTRANSFER 2 /// Return value to cancel attaching diff --git a/code/datums/components/README.md b/code/datums/components/README.md index 66b8f81e0dd..509630bb0a0 100644 --- a/code/datums/components/README.md +++ b/code/datums/components/README.md @@ -4,130 +4,6 @@ Loosely adapted from /vg/. This is an entity component system for adding behaviours to datums when inheritance doesn't quite cut it. By using signals and events instead of direct inheritance, you can inject behaviours without hacky overloads. It requires a different method of thinking, but is not hard to use correctly. If a behaviour can have application across more than one thing. Make it generic, make it a component. Atom/mob/obj event? Give it a signal, and forward it's arguments with a `SendSignal()` call. Now every component that want's to can also know about this happening. -### In the code +See [this thread](https://tgstation13.org/phpBB/viewtopic.php?f=5&t=22674) for an introduction to the system as a whole. -#### Slippery things - -At the time of this writing, every object that is slippery overrides atom/Crossed does some checks, then slips the mob. Instead of all those Crossed overrides they could add a slippery component to all these objects. And have the checks in one proc that is run by the Crossed event - -#### Powercells - -A lot of objects have powercells. The `get_cell()` proc was added to give generic access to the cell var if it had one. This is just a specific use case of `GetComponent()` - -#### Radios - -The radio object as it is should not exist, given that more things use the _concept_ of radios rather than the object itself. The actual function of the radio can exist in a component which all the things that use it (Request consoles, actual radios, the SM shard) can add to themselves. - -#### Standos - -Stands have a lot of procs which mimic mob procs. Rather than inserting hooks for all these procs in overrides, the same can be accomplished with signals - -## API - -### Defines - -1. `COMPONENT_INCOMPATIBLE` Return this from `/datum/component/Initialize` or `datum/component/OnTransfer` to have the component be deleted if it's applied to an incorrect type. `parent` must not be modified if this is to be returned. This will be noted in the runtime logs - -### Vars - -1. `/datum/var/list/datum_components` (private) - * Lazy associated list of type -> component/list of components. -1. `/datum/var/list/comp_lookup` (private) - * Lazy associated list of signal -> registree/list of registrees -1. `/datum/var/list/signal_procs` (private) - * Associated lazy list of signals -> `/datum/callback`s that will be run when the parent datum receives that signal -1. `/datum/var/signal_enabled` (protected, boolean) - * If the datum is signal enabled. If not, it will not react to signals - * `FALSE` by default, set to `TRUE` when a signal is registered -1. `/datum/component/var/dupe_mode` (protected, enum) - * How duplicate component types are handled when added to the datum. - * `COMPONENT_DUPE_HIGHLANDER` (default): Old component will be deleted, new component will first have `/datum/component/proc/InheritComponent(datum/component/old, FALSE)` on it - * `COMPONENT_DUPE_ALLOWED`: The components will be treated as separate, `GetComponent()` will return the first added - * `COMPONENT_DUPE_UNIQUE`: New component will be deleted, old component will first have `/datum/component/proc/InheritComponent(datum/component/new, TRUE)` on it - * `COMPONENT_DUPE_UNIQUE_PASSARGS`: New component will never exist and instead its initialization arguments will be passed on to the old component. -1. `/datum/component/var/dupe_type` (protected, type) - * Definition of a duplicate component type - * `null` means exact match on `type` (default) - * Any other type means that and all subtypes -1. `/datum/component/var/datum/parent` (protected, read-only) - * The datum this component belongs to - * Never `null` in child procs -1. `report_signal_origin` (protected, boolean) - * If `TRUE`, will invoke the callback when signalled with the signal type as the first argument. - * `FALSE` by default. - -### Procs - -1. `/datum/proc/GetComponent(component_type(type)) -> datum/component?` (public, final) - * Returns a reference to a component of component_type if it exists in the datum, null otherwise -1. `/datum/proc/GetComponents(component_type(type)) -> list` (public, final) - * Returns a list of references to all components of component_type that exist in the datum -1. `/datum/proc/GetExactComponent(component_type(type)) -> datum/component?` (public, final) - * Returns a reference to a component whose type MATCHES component_type if that component exists in the datum, null otherwise -1. `SEND_SIGNAL(target, sigtype, ...)` (public, final) - * Use to send signals to target datum - * Extra arguments are to be specified in the signal definition - * Returns a bitflag with signal specific information assembled from all activated components - * Arguments are packaged in a list and handed off to _SendSignal() -1. `/datum/proc/AddComponent(component_type(type), ...) -> datum/component` (public, final) - * Creates an instance of `component_type` in the datum and passes `...` to its `Initialize()` call - * Sends the `COMSIG_COMPONENT_ADDED` signal to the datum - * All components a datum owns are deleted with the datum - * Returns the component that was created. Or the old component in a dupe situation where `COMPONENT_DUPE_UNIQUE` was set - * If this tries to add an component to an incompatible type, the component will be deleted and the result will be `null`. This is very unperformant, try not to do it - * Properly handles duplicate situations based on the `dupe_mode` var -1. `/datum/proc/LoadComponent(component_type(type), ...) -> datum/component` (public, final) - * Equivalent to calling `GetComponent(component_type)` where, if the result would be `null`, returns `AddComponent(component_type, ...)` instead -1. `/datum/proc/ComponentActivated(datum/component/C)` (abstract, async) - * Called on a component's `parent` after a signal received causes it to activate. `src` is the parameter - * Will only be called if a component's callback returns `TRUE` -1. `/datum/proc/TakeComponent(datum/component/C)` (public, final) - * Properly transfers ownership of a component from one datum to another - * Signals `COMSIG_COMPONENT_REMOVING` on the parent - * Called on the datum you want to own the component with another datum's component -1. `/datum/proc/_SendSignal(signal, list/arguments)` (private, final) - * Handles most of the actual signaling procedure - * Will runtime if used on datums with an empty component list -1. `/datum/proc/RegisterSignal(datum/target, signal(string/list of strings), proc_ref(type), override(boolean))` (protected, final) - * If signal is a list it will be as if RegisterSignal was called for each of the entries with the same following arguments - * Makes the datum listen for the specified `signal` on it's `parent` datum. - * When that signal is received `proc_ref` will be called on the component, along with associated arguments - * Example proc ref: `.proc/OnEvent` - * If a previous registration is overwritten by the call, a runtime occurs. Setting `override` to TRUE prevents this - * These callbacks run asyncronously - * Returning `TRUE` from these callbacks will trigger a `TRUE` return from the `SendSignal()` that initiated it -1. `/datum/component/New(datum/parent, ...)` (private, final) - * Runs internal setup for the component - * Extra arguments are passed to `Initialize()` -1. `/datum/component/Initialize(...)` (abstract, no-sleep) - * Called by `New()` with the same argments excluding `parent` - * Component does not exist in `parent`'s `datum_components` list yet, although `parent` is set and may be used - * Signals will not be received while this function is running - * Component may be deleted after this function completes without being attached - * Do not call `qdel(src)` from this function -1. `/datum/component/Destroy(force(bool), silent(bool))` (virtual, no-sleep) - * Sends the `COMSIG_COMPONENT_REMOVING` signal to the parent datum if the `parent` isn't being qdeleted - * Properly removes the component from `parent` and cleans up references - * Setting `force` makes it not check for and remove the component from the parent - * Setting `silent` deletes the component without sending a `COMSIG_COMPONENT_REMOVING` signal -1. `/datum/component/proc/InheritComponent(datum/component/C, i_am_original(boolean))` (abstract, no-sleep) - * Called on a component when a component of the same type was added to the same parent - * See `/datum/component/var/dupe_mode` - * `C`'s type will always be the same of the called component -1. `/datum/component/proc/AfterComponentActivated()` (abstract, async) - * Called on a component that was activated after it's `parent`'s `ComponentActivated()` is called -1. `/datum/component/proc/OnTransfer(datum/new_parent)` (abstract, no-sleep) - * Called before `new_parent` is assigned to `parent` in `TakeComponent()` - * Allows the component to react to ownership transfers -1. `/datum/component/proc/_RemoveFromParent()` (private, final) - * Clears `parent` and removes the component from it's component list -1. `/datum/component/proc/_JoinParent` (private, final) - * Tries to add the component to it's `parent`s `datum_components` list -1. `/datum/component/proc/RegisterWithParent` (abstract, no-sleep) - * Used to register the signals that should be on the `parent` object - * Use this if you plan on the component transfering between parents -1. `/datum/component/proc/UnregisterFromParent` (abstract, no-sleep) - * Counterpart to `RegisterWithParent()` - * Used to unregister the signals that should only be on the `parent` object - -### See/Define signals and their arguments in __DEFINES\components.dm +### See/Define signals and their arguments in [__DEFINES\components.dm](..\..\__DEFINES\components.dm) diff --git a/code/datums/components/_component.dm b/code/datums/components/_component.dm index 818027b1998..13c8a733c28 100644 --- a/code/datums/components/_component.dm +++ b/code/datums/components/_component.dm @@ -3,21 +3,33 @@ * * The component datum * - * a component should be a single standalone unit + * A component should be a single standalone unit * of functionality, that works by receiving signals from it's parent * object to provide some single functionality (i.e a slippery component) - * that makes the object it's attached to cause people to slip over + * that makes the object it's attached to cause people to slip over. + * Useful when you want shared behaviour independent of type inheritance */ /datum/component + /// Defines how duplicate existing components are handled when added to a datum + /// See `COMPONENT_DUPE_*` definitions for available options var/dupe_mode = COMPONENT_DUPE_HIGHLANDER + + /// The type to check for duplication + /// `null` means exact match on `type` (default) + /// Any other type means that and all subtypes var/dupe_type + + /// The datum this components belongs to var/datum/parent - //only set to true if you are able to properly transfer this component - //At a minimum RegisterWithParent and UnregisterFromParent should be used - //Make sure you also implement PostTransfer for any post transfer handling + + /// Only set to true if you are able to properly transfer this component + /// At a minimum RegisterWithParent and UnregisterFromParent should be used + /// Make sure you also implement PostTransfer for any post transfer handling var/can_transfer = FALSE + /** - * Create a new component + * Create a new component. + * Additional arguments are passed to `Initialize()` * * Arguments: * * datum/P the parent datum this component reacts to signals from @@ -31,6 +43,29 @@ _JoinParent(P) +/** + * Called during component creation with the same arguments as in new excluding parent. + * Do not call `qdel(src)` from this function, `return COMPONENT_INCOMPATIBLE` instead + */ +/datum/component/proc/Initialize(...) + return + +/** + * Properly removes the component from `parent` and cleans up references + * Setting `force` makes it not check for and remove the component from the parent + * Setting `silent` deletes the component without sending a `COMSIG_COMPONENT_REMOVING` signal + */ +/datum/component/Destroy(force=FALSE, silent=FALSE) + if(!force && parent) + _RemoveFromParent() + if(!silent) + SEND_SIGNAL(parent, COMSIG_COMPONENT_REMOVING, src) + parent = null + return ..() + +/** + * Internal proc to handle behaviour of components when joining a parent + */ /datum/component/proc/_JoinParent() var/datum/P = parent //lazy init the parent's dc list @@ -66,28 +101,9 @@ RegisterWithParent() -// If you want/expect to be moving the component around between parents, use this to register on the parent for signals /** - * Register the components signals with the parent object - * - * Use this proc to register signals with your parent object - * this is called when an object is created, or is transferred - * between objects + * Internal proc to handle behaviour when being removed from a parent */ -/datum/component/proc/RegisterWithParent() - return - -/datum/component/proc/Initialize(...) - return - -/datum/component/Destroy(force=FALSE, silent=FALSE) - if(!force && parent) - _RemoveFromParent() - if(!silent) - SEND_SIGNAL(parent, COMSIG_COMPONENT_REMOVING, src) - parent = null - return ..() - /datum/component/proc/_RemoveFromParent() var/datum/P = parent var/list/dc = P.datum_components @@ -107,11 +123,19 @@ UnregisterFromParent() /** - * Unregister signals from our parent object + * Register the component with the parent object * - * Use this proc to unregister for signals from your parent object - * this is called when an object is destroyed, or is transferred - * between objects + * Use this proc to register with your parent object + * Overridable proc that's called when added to a new parent + */ +/datum/component/proc/RegisterWithParent() + return + +/** + * Unregister from our parent object + * + * Use this proc to unregister from your parent object + * Overridable proc that's called when removed from a parent * * */ /datum/component/proc/UnregisterFromParent() @@ -122,12 +146,13 @@ * * This sets up a listening relationship such that when the target object emits a signal * the source datum this proc is called upon, will recieve a callback to the given proctype + * Return values from procs registered must be a bitfield * * Arguments: * * datum/target The target to listen for signals from * * sig_type_or_types Either a string signal name, or a list of signal names (strings) * * proctype The proc to call back when the signal is emitted - * * override TODO, chase ninja to ask + * * override If a previous registration exists you must explicitly set this */ /datum/proc/RegisterSignal(datum/target, sig_type_or_types, proctype, override = FALSE) if(QDELETED(src) || QDELETED(target)) @@ -160,10 +185,12 @@ lookup[sig_type][src] = TRUE signal_enabled = TRUE + /** * Stop listening to a given signal from target * * Breaks the relationship between target and source datum, removing the callback when the signal fires + * Doesn't care if a registration exists or not * * Arguments: * * datum/target Datum to stop listening to signals from @@ -198,8 +225,14 @@ if(!signal_procs[target].len) signal_procs -= target +/** + * Called on a component when a component of the same type was added to the same parent + * See `/datum/component/var/dupe_mode` + * `C`'s type will always be the same of the called component + */ /datum/component/proc/InheritComponent(datum/component/C, i_am_original) return + /** * Callback Just before this component is transferred * @@ -208,15 +241,20 @@ */ /datum/component/proc/PreTransfer() return + /** * Callback Just after a component is transferred * * Use this to do any special setup you need to do after being moved to a new object + * Do not call `qdel(src)` from this function, `return COMPONENT_INCOMPATIBLE` instead * */ /datum/component/proc/PostTransfer() return COMPONENT_INCOMPATIBLE //Do not support transfer by default as you must properly support it +/** + * Internal proc to create a list of our type and all parent types + */ /datum/component/proc/_GetInverseTypeList(our_type = type) //we can do this one simple trick var/current_type = parent_type @@ -226,6 +264,11 @@ current_type = type2parent(current_type) . += current_type +/** + * Internal proc to handle most all of the signaling procedure + * Will runtime if used on datums with an empty component list + * Use the `SEND_SIGNAL` define instead + */ /datum/proc/_SendSignal(sigtype, list/arguments) var/target = comp_lookup[sigtype] if(!length(target)) @@ -245,6 +288,7 @@ // The type arg is casted so initial works, you shouldn't be passing a real instance into this /** * Return any component assigned to this datum of the given type + * This will throw an error if it's possible to have more than one component of that type on the parent * * Arguments: * * datum/component/c_type The typepath of the component you want to get a reference to @@ -260,7 +304,18 @@ if(length(.)) return .[1] -/datum/proc/GetExactComponent(c_type) +// The type arg is casted so initial works, you shouldn't be passing a real instance into this +/** + * Return any component assigned to this datum of the exact given type + * This will throw an error if it's possible to have more than one component of that type on the parent + * + * Arguments: + * * datum/component/c_type The typepath of the component you want to get a reference to + */ +/datum/proc/GetExactComponent(datum/component/c_type) + RETURN_TYPE(c_type) + if(initial(c_type.dupe_mode) == COMPONENT_DUPE_ALLOWED) + stack_trace("GetComponent was called to get a component of which multiple copies could be on an object. This can easily break and should be changed. Type: \[[c_type]\]") var/list/dc = datum_components if(!dc) return null @@ -286,6 +341,13 @@ if(!length(.)) return list(.) +/** + * Creates an instance of `new_type` in the datum and attaches to it as parent + * Sends the `COMSIG_COMPONENT_ADDED` signal to the datum + * Returns the component that was created. Or the old component in a dupe situation where `COMPONENT_DUPE_UNIQUE` was set + * If this tries to add an component to an incompatible type, the component will be deleted and the result will be `null`. This is very unperformant, try not to do it + * Properly handles duplicate situations based on the `dupe_mode` var + */ /datum/proc/AddComponent(new_type, ...) var/datum/component/nt = new_type var/dm = initial(nt.dupe_mode) @@ -351,8 +413,9 @@ . = GetComponent(component_type) if(!.) return AddComponent(arglist(args)) + /** - * Remove this component from it's parent + * Removes the component from parent, ends up with a null parent */ /datum/component/proc/RemoveComponent() if(!parent) @@ -386,6 +449,7 @@ if(target == AddComponent(target)) target._JoinParent() + /** * Transfer all components to target * @@ -407,6 +471,9 @@ var/datum/component/C = comps if(C.can_transfer) target.TakeComponent(comps) -///Return the object that is the host of any UI's that this component has + +/** + * Return the object that is the host of any UI's that this component has + */ /datum/component/ui_host() return parent diff --git a/code/datums/datum.dm b/code/datums/datum.dm index a03e9c50c9d..a1db428551b 100644 --- a/code/datums/datum.dm +++ b/code/datums/datum.dm @@ -19,16 +19,21 @@ /// Active timers with this datum as the target var/list/active_timers - /// Components attached to this datum - var/list/datum_components /// Status traits attached to this datum var/list/status_traits + + /// Components attached to this datum + /// Lazy associated list in the structure of `type:component/list of components` + var/list/datum_components /// Any datum registered to receive signals from this datum is in this list + /// Lazy associated list in the structure of `signal:registree/list of registrees` var/list/comp_lookup - /// List of callbacks for signal procs + /// Lazy associated list in the structure of `signals:proctype` that are run when the datum receives that signal var/list/list/datum/callback/signal_procs /// Is this datum capable of sending signals? + /// Set to true when a signal has been registered var/signal_enabled = FALSE + /// Datum level flags var/datum_flags = NONE