diff --git a/aurorastation.dme b/aurorastation.dme
index 3488fcce968..af98681d0e4 100644
--- a/aurorastation.dme
+++ b/aurorastation.dme
@@ -62,6 +62,9 @@
#include "code\__defines\vueui.dm"
#include "code\__defines\webhook.dm"
#include "code\__defines\ZAS.dm"
+#include "code\__defines\dcs\flags.dm"
+#include "code\__defines\dcs\helpers.dm"
+#include "code\__defines\dcs\signals.dm"
#include "code\_helpers\_global_objects.dm"
#include "code\_helpers\_string_lists.dm"
#include "code\_helpers\area_movement.dm"
@@ -228,6 +231,7 @@
#include "code\controllers\subsystems\initialization\xenoarch.dm"
#include "code\controllers\subsystems\processing\airflow.dm"
#include "code\controllers\subsystems\processing\calamity.dm"
+#include "code\controllers\subsystems\processing\dcs.dm"
#include "code\controllers\subsystems\processing\disease.dm"
#include "code\controllers\subsystems\processing\disposals.dm"
#include "code\controllers\subsystems\processing\electronics.dm"
@@ -267,6 +271,11 @@
#include "code\datums\brain_damage\schizo.dm"
#include "code\datums\brain_damage\severe.dm"
#include "code\datums\brain_damage\special_ed.dm"
+#include "code\datums\components\_component.dm"
+#include "code\datums\components\multitool\_multitool.dm"
+#include "code\datums\components\multitool\multitool.dm"
+#include "code\datums\components\multitool\circuitboards\circuitboards.dm"
+#include "code\datums\components\multitool\circuitboards\stationalert.dm"
#include "code\datums\discord\bot.dm"
#include "code\datums\discord\webhook.dm"
#include "code\datums\diseases\appendicitis.dm"
@@ -304,19 +313,11 @@
#include "code\datums\diseases\advance\symptoms\voice_change.dm"
#include "code\datums\diseases\advance\symptoms\vomit.dm"
#include "code\datums\diseases\advance\symptoms\weight.dm"
+#include "code\datums\elements\_element.dm"
#include "code\datums\ert\nanotrasen.dm"
#include "code\datums\ert\outsider.dm"
#include "code\datums\ert\responseteam.dm"
#include "code\datums\ert\tcfl.dm"
-#include "code\datums\expansions\expansion.dm"
-#include "code\datums\expansions\multitool\_multitool.dm"
-#include "code\datums\expansions\multitool\multitool.dm"
-#include "code\datums\expansions\multitool\store.dm"
-#include "code\datums\expansions\multitool\circuitboards\circuitboards.dm"
-#include "code\datums\expansions\multitool\circuitboards\stationalert.dm"
-#include "code\datums\expansions\multitool\items\cable.dm"
-#include "code\datums\expansions\multitool\items\items.dm"
-#include "code\datums\expansions\multitool\machinery\cloning.dm"
#include "code\datums\helper_datums\away_mission.dm"
#include "code\datums\helper_datums\construction_datum.dm"
#include "code\datums\helper_datums\events.dm"
diff --git a/code/__defines/dcs/flags.dm b/code/__defines/dcs/flags.dm
new file mode 100644
index 00000000000..a9e4c97885f
--- /dev/null
+++ b/code/__defines/dcs/flags.dm
@@ -0,0 +1,30 @@
+/// 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
+#define ELEMENT_INCOMPATIBLE 1
+
+// /datum/element flags
+/// Causes the detach proc to be called when the host object is being deleted
+#define ELEMENT_DETACH (1 << 0)
+/**
+ * Only elements created with the same arguments given after `id_arg_index` share an element instance
+ * The arguments are the same when the text and number values are the same and all other values have the same ref
+ */
+#define ELEMENT_BESPOKE (1 << 1)
+
+// How multiple components of the exact same type are handled in the same datum
+/// old component is deleted (default)
+#define COMPONENT_DUPE_HIGHLANDER 0
+/// duplicates allowed
+#define COMPONENT_DUPE_ALLOWED 1
+/// new component is deleted
+#define COMPONENT_DUPE_UNIQUE 2
+/// old component is given the initialization args of the new
+#define COMPONENT_DUPE_UNIQUE_PASSARGS 4
+/// each component of the same type is consulted as to whether the duplicate should be allowed
+#define COMPONENT_DUPE_SELECTIVE 5
diff --git a/code/__defines/dcs/helpers.dm b/code/__defines/dcs/helpers.dm
new file mode 100644
index 00000000000..b13530cdf74
--- /dev/null
+++ b/code/__defines/dcs/helpers.dm
@@ -0,0 +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) )
+
+/// A wrapper for _AddElement that allows us to pretend we're using normal named arguments
+#define AddElement(arguments...) _AddElement(list(##arguments))
+
+/// A wrapper for _RemoveElement that allows us to pretend we're using normal named arguments
+#define RemoveElement(arguments...) _RemoveElement(list(##arguments))
+
+/// A wrapper for _AddComponent that allows us to pretend we're using normal named arguments
+#define AddComponent(arguments...) _AddComponent(list(##arguments))
\ No newline at end of file
diff --git a/code/__defines/dcs/signals.dm b/code/__defines/dcs/signals.dm
new file mode 100644
index 00000000000..64bef3834fa
--- /dev/null
+++ b/code/__defines/dcs/signals.dm
@@ -0,0 +1,32 @@
+// All signals. Format:
+// When the signal is called: (signal arguments)
+// All signals send the source datum of the signal as the first argument
+
+// global signals
+// These are signals which can be listened to by any component on any parent
+
+//////////////////////////////////////////////////////////////////
+
+// /datum signals
+/// when a component is added to a datum: (/datum/component)
+#define COMSIG_COMPONENT_ADDED "component_added"
+/// before a component is removed from a datum because of RemoveComponent: (/datum/component)
+#define COMSIG_COMPONENT_REMOVING "component_removing"
+/// before a datum's Destroy() is called: (force), returning a nonzero value will cancel the qdel operation
+#define COMSIG_PARENT_PREQDELETED "parent_preqdeleted"
+/// just before a datum's Destroy() is called: (force), at this point none of the other components chose to interrupt qdel and Destroy will be called
+#define COMSIG_PARENT_QDELETING "parent_qdeleting"
+
+// /atom signals
+
+// /area signals
+
+// /turf signals
+
+// /atom/movable signals
+
+// /mob signals
+
+// /obj signals
+
+/*******Component Specific Signals*******/
diff --git a/code/__defines/misc.dm b/code/__defines/misc.dm
index e3caf832e1a..2d8535bd220 100644
--- a/code/__defines/misc.dm
+++ b/code/__defines/misc.dm
@@ -1,5 +1,9 @@
#define DEBUG
+// Flags
+#define ALL (~0) //For convenience.
+#define NONE 0
+
// Turf-only flags.
#define NOJAUNT 1 // This is used in literally one place, turf.dm, to block ethereal jaunt.
#define MIMIC_BELOW 2 // If this turf should mimic the turf on the Z below.
diff --git a/code/_helpers/type2type.dm b/code/_helpers/type2type.dm
index 5fa6d17488a..61ca2daa2c5 100644
--- a/code/_helpers/type2type.dm
+++ b/code/_helpers/type2type.dm
@@ -298,4 +298,19 @@ proc/tg_list2text(list/list, glue=",")
var/r = hex2num(textr)
var/g = hex2num(textg)
var/b = hex2num(textb)
- return "rgba([r], [g], [b], [alpha])"
\ No newline at end of file
+ return "rgba([r], [g], [b], [alpha])"
+
+/proc/type2parent(child)
+ var/string_type = "[child]"
+ var/last_slash = findlasttext(string_type, "/")
+ if(last_slash == 1)
+ switch(child)
+ if(/datum)
+ return null
+ if(/obj || /mob)
+ return /atom/movable
+ if(/area || /turf)
+ return /atom
+ else
+ return /datum
+ return text2path(copytext(string_type, 1, last_slash))
\ No newline at end of file
diff --git a/code/_onclick/ai.dm b/code/_onclick/ai.dm
index 6126c4036c2..98de3f185a7 100644
--- a/code/_onclick/ai.dm
+++ b/code/_onclick/ai.dm
@@ -56,7 +56,7 @@
if(multitool_mode && isobj(A))
var/obj/O = A
- var/datum/expansion/multitool/MT = LAZYACCESS(O.expansions, /datum/expansion/multitool)
+ var/datum/component/multitool/MT = O.GetComponent(/datum/component/multitool)
if(MT)
MT.interact(ai_multi, src)
return
diff --git a/code/controllers/subsystems/garbage.dm b/code/controllers/subsystems/garbage.dm
index 9fa43dff4c1..0a85e91e1ad 100644
--- a/code/controllers/subsystems/garbage.dm
+++ b/code/controllers/subsystems/garbage.dm
@@ -187,9 +187,12 @@ var/datum/controller/subsystem/garbage_collector/SSgarbage
if(!istype(D))
del(D)
else if(isnull(D.gcDestroyed))
+ if (SEND_SIGNAL(D, COMSIG_PARENT_PREQDELETED, force)) // Give the components a chance to prevent their parent from being deleted
+ return
D.gcDestroyed = GC_CURRENTLY_BEING_QDELETED
var/start_time = world.time
var/hint = D.Destroy(force) // Let our friend know they're about to get fucked up.
+ SEND_SIGNAL(D, COMSIG_PARENT_QDELETING, force) // Let the (remaining) components know about the result of Destroy
if(world.time != start_time)
SSgarbage.sleptDestroy["[D.type]"]++
if(!D)
diff --git a/code/controllers/subsystems/processing/dcs.dm b/code/controllers/subsystems/processing/dcs.dm
new file mode 100644
index 00000000000..4c071985be9
--- /dev/null
+++ b/code/controllers/subsystems/processing/dcs.dm
@@ -0,0 +1,58 @@
+/var/datum/controller/subsystem/processing/dcs/SSdcs
+
+/datum/controller/subsystem/processing/dcs
+ name = "Datum Component System"
+ flags = SS_NO_INIT
+
+ var/list/elements_by_type = list()
+
+/datum/controller/subsystem/processing/dcs/New()
+ NEW_SS_GLOBAL(SSdcs)
+
+/datum/controller/subsystem/processing/dcs/Recover()
+ comp_lookup = SSdcs.comp_lookup
+
+/datum/controller/subsystem/processing/dcs/proc/GetElement(list/arguments)
+ var/datum/element/eletype = arguments[1]
+ var/element_id = eletype
+
+ if(!ispath(eletype, /datum/element))
+ CRASH("Attempted to instantiate [eletype] as a /datum/element")
+
+ if(initial(eletype.element_flags) & ELEMENT_BESPOKE)
+ element_id = GetIdFromArguments(arguments)
+
+ . = elements_by_type[element_id]
+ if(.)
+ return
+ . = elements_by_type[element_id] = new eletype
+
+/****
+ * Generates an id for bespoke elements when given the argument list
+ * Generating the id here is a bit complex because we need to support named arguments
+ * Named arguments can appear in any order and we need them to appear after ordered arguments
+ * We assume that no one will pass in a named argument with a value of null
+ **/
+/datum/controller/subsystem/processing/dcs/proc/GetIdFromArguments(list/arguments)
+ var/datum/element/eletype = arguments[1]
+ var/list/fullid = list("[eletype]")
+ var/list/named_arguments = list()
+ for(var/i in initial(eletype.id_arg_index) to length(arguments))
+ var/key = arguments[i]
+ var/value
+ if(istext(key))
+ value = arguments[key]
+ if(!(istext(key) || isnum(key)))
+ key = ref(key)
+ key = "[key]" // Key is stringified so numbers dont break things
+ if(!isnull(value))
+ if(!(istext(value) || isnum(value)))
+ value = ref(value)
+ named_arguments["[key]"] = value
+ else
+ fullid += "[key]"
+
+ if(length(named_arguments))
+ named_arguments = sortList(named_arguments)
+ fullid += named_arguments
+ return list2params(fullid)
diff --git a/code/datums/components/README.md b/code/datums/components/README.md
new file mode 100644
index 00000000000..3080f7e7b44
--- /dev/null
+++ b/code/datums/components/README.md
@@ -0,0 +1,9 @@
+# Datum Component System (DCS)
+
+## Concept
+
+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.
+
+See [this thread](https://tgstation13.org/phpBB/viewtopic.php?f=5&t=22674) for an introduction to the system as a whole.
+
+### 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
new file mode 100644
index 00000000000..1b99b99dfc0
--- /dev/null
+++ b/code/datums/components/_component.dm
@@ -0,0 +1,507 @@
+/**
+ * # Component
+ *
+ * The component datum
+ *
+ * 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.
+ * 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
+ var/can_transfer = FALSE
+
+/**
+ * Create a new component.
+ *
+ * Additional arguments are passed to [Initialize()][/datum/component/proc/Initialize]
+ *
+ * Arguments:
+ * * datum/P the parent datum this component reacts to signals from
+ */
+/datum/component/New(list/raw_args)
+ parent = raw_args[1]
+ var/list/arguments = raw_args.Copy(2)
+ if(Initialize(arglist(arguments)) == COMPONENT_INCOMPATIBLE)
+ crash_with("Incompatible [type] assigned to a [parent.type]! args: [json_encode(arguments)]")
+ qdel(src, TRUE, TRUE)
+ return
+
+ _JoinParent(parent)
+
+/**
+ * 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
+ var/list/dc = P.datum_components
+ if(!dc)
+ P.datum_components = dc = list()
+
+ //set up the typecache
+ var/our_type = type
+ for(var/I in _GetInverseTypeList(our_type))
+ var/test = dc[I]
+ if(test) //already another component of this type here
+ var/list/components_of_type
+ if(!length(test))
+ components_of_type = list(test)
+ dc[I] = components_of_type
+ else
+ components_of_type = test
+ if(I == our_type) //exact match, take priority
+ var/inserted = FALSE
+ for(var/J in 1 to components_of_type.len)
+ var/datum/component/C = components_of_type[J]
+ if(C.type != our_type) //but not over other exact matches
+ components_of_type.Insert(J, I)
+ inserted = TRUE
+ break
+ if(!inserted)
+ components_of_type += src
+ else //indirect match, back of the line with ya
+ components_of_type += src
+ else //only component of this type, no list
+ dc[I] = src
+
+ RegisterWithParent()
+
+/**
+ * Internal proc to handle behaviour when being removed from a parent
+ */
+/datum/component/proc/_RemoveFromParent()
+ var/datum/P = parent
+ var/list/dc = P.datum_components
+ for(var/I in _GetInverseTypeList())
+ var/list/components_of_type = dc[I]
+ if(length(components_of_type)) //
+ var/list/subtracted = components_of_type - src
+ if(subtracted.len == 1) //only 1 guy left
+ dc[I] = subtracted[1] //make him special
+ else
+ dc[I] = subtracted
+ else //just us
+ dc -= I
+ if(!dc.len)
+ P.datum_components = null
+
+ UnregisterFromParent()
+
+/**
+ * Register the component with the parent object
+ *
+ * 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()
+ return
+
+/**
+ * Register to listen for a signal from the passed in target
+ *
+ * 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 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))
+ return
+
+ var/list/procs = signal_procs
+ if(!procs)
+ signal_procs = procs = list()
+ if(!procs[target])
+ procs[target] = list()
+ var/list/lookup = target.comp_lookup
+ if(!lookup)
+ target.comp_lookup = lookup = list()
+
+ var/list/sig_types = islist(sig_type_or_types) ? sig_type_or_types : list(sig_type_or_types)
+ for(var/sig_type in sig_types)
+ if(!override && procs[target][sig_type])
+ crash_with("[sig_type] overridden. Use override = TRUE to suppress this warning")
+
+ procs[target][sig_type] = proctype
+
+ if(!lookup[sig_type]) // Nothing has registered here yet
+ lookup[sig_type] = src
+ else if(lookup[sig_type] == src) // We already registered here
+ continue
+ else if(!length(lookup[sig_type])) // One other thing registered here
+ lookup[sig_type] = list(lookup[sig_type]=TRUE)
+ lookup[sig_type][src] = TRUE
+ else // Many other things have registered here
+ 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
+ * * sig_typeor_types Signal string key or list of signal keys to stop listening to specifically
+ */
+/datum/proc/UnregisterSignal(datum/target, sig_type_or_types)
+ var/list/lookup = target.comp_lookup
+ if(!signal_procs || !signal_procs[target] || !lookup)
+ return
+ if(!islist(sig_type_or_types))
+ sig_type_or_types = list(sig_type_or_types)
+ for(var/sig in sig_type_or_types)
+ switch(length(lookup[sig]))
+ if(2)
+ lookup[sig] = (lookup[sig]-src)[1]
+ if(1)
+ crash_with("[target] ([target.type]) somehow has single length list inside comp_lookup")
+ if(src in lookup[sig])
+ lookup -= sig
+ if(!length(lookup))
+ target.comp_lookup = null
+ break
+ if(0)
+ lookup -= sig
+ if(!length(lookup))
+ target.comp_lookup = null
+ break
+ else
+ lookup[sig] -= src
+
+ signal_procs[target] -= sig_type_or_types
+ 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
+
+/**
+ * Called on a component when a component of the same type was added to the same parent with [COMPONENT_DUPE_SELECTIVE]
+ *
+ * See [/datum/component/var/dupe_mode]
+ *
+ * `C`'s type will always be the same of the called component
+ *
+ * return TRUE if you are absorbing the component, otherwise FALSE if you are fine having it exist as a duplicate component
+ */
+/datum/component/proc/CheckDupeComponent(datum/component/C, ...)
+ return
+
+/**
+ * Callback Just before this component is transferred
+ *
+ * Use this to do any special cleanup you might need to do before being deregged from an object
+ *
+ */
+/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
+ . = list(our_type, current_type)
+ //and since most components are root level + 1, this won't even have to run
+ while (current_type != /datum/component)
+ 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))
+ var/datum/C = target
+ if(!C.signal_enabled)
+ return NONE
+ var/proctype = C.signal_procs[src][sigtype]
+ return NONE | INVOKE_ASYNC(C, proctype, arguments)
+ . = NONE
+ for(var/I in target)
+ var/datum/C = I
+ if(!C.signal_enabled)
+ continue
+ var/proctype = C.signal_procs[src][sigtype]
+ . |= INVOKE_ASYNC(C, proctype, arguments)
+
+// 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
+ */
+/datum/proc/GetComponent(datum/component/c_type)
+ RETURN_TYPE(c_type)
+ if(initial(c_type.dupe_mode) == COMPONENT_DUPE_ALLOWED)
+ crash_with("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
+ . = dc[c_type]
+ if(length(.))
+ return .[1]
+
+// 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)
+ crash_with("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
+ var/datum/component/C = dc[c_type]
+ if(C)
+ if(length(C))
+ C = C[1]
+ if(C.type == c_type)
+ return C
+ return null
+
+/**
+ * Get all components of a given type that are attached to this datum
+ *
+ * Arguments:
+ * * c_type The component type path
+ */
+/datum/proc/GetComponents(c_type)
+ var/list/dc = datum_components
+ if(!dc)
+ return null
+ . = dc[c_type]
+ 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(list/raw_args)
+ var/new_type = raw_args[1]
+ var/datum/component/nt = new_type
+ var/dm = initial(nt.dupe_mode)
+ var/dt = initial(nt.dupe_type)
+
+ var/datum/component/old_comp
+ var/datum/component/new_comp
+
+ if(ispath(nt))
+ if(nt == /datum/component)
+ CRASH("[nt] attempted instantiation!")
+ else
+ new_comp = nt
+ nt = new_comp.type
+
+ raw_args[1] = src
+
+ if(dm != COMPONENT_DUPE_ALLOWED)
+ if(!dt)
+ old_comp = GetExactComponent(nt)
+ else
+ old_comp = GetComponent(dt)
+ if(old_comp)
+ switch(dm)
+ if(COMPONENT_DUPE_UNIQUE)
+ if(!new_comp)
+ new_comp = new nt(raw_args)
+ if(!QDELETED(new_comp))
+ old_comp.InheritComponent(new_comp, TRUE)
+ QDEL_NULL(new_comp)
+ if(COMPONENT_DUPE_HIGHLANDER)
+ if(!new_comp)
+ new_comp = new nt(raw_args)
+ if(!QDELETED(new_comp))
+ new_comp.InheritComponent(old_comp, FALSE)
+ QDEL_NULL(old_comp)
+ if(COMPONENT_DUPE_UNIQUE_PASSARGS)
+ if(!new_comp)
+ var/list/arguments = raw_args.Copy(2)
+ arguments.Insert(1, null, TRUE)
+ old_comp.InheritComponent(arglist(arguments))
+ else
+ old_comp.InheritComponent(new_comp, TRUE)
+ if(COMPONENT_DUPE_SELECTIVE)
+ var/list/arguments = raw_args.Copy()
+ arguments[1] = new_comp
+ var/make_new_component = TRUE
+ for(var/i in GetComponents(new_type))
+ var/datum/component/C = i
+ if(C.CheckDupeComponent(arglist(arguments)))
+ make_new_component = FALSE
+ QDEL_NULL(new_comp)
+ break
+ if(!new_comp && make_new_component)
+ new_comp = new nt(raw_args)
+ else if(!new_comp)
+ new_comp = new nt(raw_args) // There's a valid dupe mode but there's no old component, act like normal
+ else if(!new_comp)
+ new_comp = new nt(raw_args) // Dupes are allowed, act like normal
+
+ if(!old_comp && !QDELETED(new_comp)) // Nothing related to duplicate components happened and the new component is healthy
+ SEND_SIGNAL(src, COMSIG_COMPONENT_ADDED, new_comp)
+ return new_comp
+ return old_comp
+
+/**
+ * Get existing component of type, or create it and return a reference to it
+ *
+ * Use this if the item needs to exist at the time of this call, but may not have been created before now
+ *
+ * Arguments:
+ * * component_type The typepath of the component to create or return
+ * * ... additional arguments to be passed when creating the component if it does not exist
+ */
+/datum/proc/LoadComponent(component_type, ...)
+ . = GetComponent(component_type)
+ if(!.)
+ return _AddComponent(args)
+
+/**
+ * Removes the component from parent, ends up with a null parent
+ */
+/datum/component/proc/RemoveComponent()
+ if(!parent)
+ return
+ var/datum/old_parent = parent
+ PreTransfer()
+ _RemoveFromParent()
+ parent = null
+ SEND_SIGNAL(old_parent, COMSIG_COMPONENT_REMOVING, src)
+
+/**
+ * Transfer this component to another parent
+ *
+ * Component is taken from source datum
+ *
+ * Arguments:
+ * * datum/component/target Target datum to transfer to
+ */
+/datum/proc/TakeComponent(datum/component/target)
+ if(!target || target.parent == src)
+ return
+ if(target.parent)
+ target.RemoveComponent()
+ target.parent = src
+ var/result = target.PostTransfer()
+ switch(result)
+ if(COMPONENT_INCOMPATIBLE)
+ var/c_type = target.type
+ qdel(target)
+ CRASH("Incompatible [c_type] transfer attempt to a [type]!")
+
+ if(target == AddComponent(target))
+ target._JoinParent()
+
+/**
+ * Transfer all components to target
+ *
+ * All components from source datum are taken
+ *
+ * Arguments:
+ * * /datum/target the target to move the components to
+ */
+/datum/proc/TransferComponents(datum/target)
+ var/list/dc = datum_components
+ if(!dc)
+ return
+ var/comps = dc[/datum/component]
+ if(islist(comps))
+ for(var/datum/component/I in comps)
+ if(I.can_transfer)
+ target.TakeComponent(I)
+ else
+ 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
+ */
+/datum/component/ui_host()
+ return parent
diff --git a/code/datums/expansions/multitool/_multitool.dm b/code/datums/components/multitool/_multitool.dm
similarity index 100%
rename from code/datums/expansions/multitool/_multitool.dm
rename to code/datums/components/multitool/_multitool.dm
diff --git a/code/datums/expansions/multitool/circuitboards/circuitboards.dm b/code/datums/components/multitool/circuitboards/circuitboards.dm
similarity index 54%
rename from code/datums/expansions/multitool/circuitboards/circuitboards.dm
rename to code/datums/components/multitool/circuitboards/circuitboards.dm
index 5d119a2b9de..379103d70e9 100644
--- a/code/datums/expansions/multitool/circuitboards/circuitboards.dm
+++ b/code/datums/components/multitool/circuitboards/circuitboards.dm
@@ -1,4 +1,4 @@
-/datum/expansion/multitool/circuitboards/CanUseTopic(var/mob/user)
+/datum/component/multitool/circuitboards/CanUseTopic(var/mob/user)
if(isAI(user)) // No remote AI access
return STATUS_CLOSE
diff --git a/code/datums/components/multitool/circuitboards/stationalert.dm b/code/datums/components/multitool/circuitboards/stationalert.dm
new file mode 100644
index 00000000000..2ea1933639a
--- /dev/null
+++ b/code/datums/components/multitool/circuitboards/stationalert.dm
@@ -0,0 +1,31 @@
+/obj/item/circuitboard/stationalert/Initialize()
+ . = ..()
+ AddComponent(/datum/component/multitool/circuitboards, CALLBACK(src, .proc/get_multitool_ui), CALLBACK(src, .proc/on_topic))
+
+/obj/item/circuitboard/stationalert/proc/get_multitool_ui(var/mob/user, var/obj/item/device/multitool/MT, var/datum/component/multitool/C)
+ . += "Alarm Sources
"
+ . += "
"
+ for(var/datum/alarm_handler/AH in SSalarm.all_handlers)
+ . += ""
+ . += "| [AH.category] | "
+ if(AH in alarm_handlers)
+ . += "◼Active | Inactivate | "
+ else
+ . += "◼Inactive | Activate | "
+ . += "
"
+ . += "
"
+
+/obj/item/circuitboard/stationalert/proc/on_topic(href, href_list, var/mob/user, var/datum/component/multitool/MT)
+ if(href_list["add"])
+ var/datum/alarm_handler/AH = locate(href_list["add"]) in SSalarm.all_handlers
+ if(AH)
+ alarm_handlers |= AH
+ return MT_REFRESH
+
+ if(href_list["remove"])
+ var/datum/alarm_handler/AH = locate(href_list["remove"]) in SSalarm.all_handlers
+ if(AH)
+ alarm_handlers -= AH
+ return MT_REFRESH
+
+ return MT_NOACTION
diff --git a/code/datums/expansions/multitool/multitool.dm b/code/datums/components/multitool/multitool.dm
similarity index 58%
rename from code/datums/expansions/multitool/multitool.dm
rename to code/datums/components/multitool/multitool.dm
index 6b319eb5451..3a97694a5eb 100644
--- a/code/datums/expansions/multitool/multitool.dm
+++ b/code/datums/components/multitool/multitool.dm
@@ -1,21 +1,18 @@
-/datum/expansion/multitool
+/datum/component/multitool
var/window_x = 370
var/window_y = 470
- var/list/interact_predicates
+ var/datum/callback/callback_get_interact_window
+ var/datum/callback/on_topic_interaction
-/datum/expansion/multitool/New(var/atom/holder, var/list/can_interact_predicates)
- ..()
- interact_predicates = can_interact_predicates ? can_interact_predicates : list()
+/datum/component/multitool/Initialize(var/datum/callback/interact_window, var/datum/callback/new_on_topic)
+ callback_get_interact_window = interact_window
+ on_topic_interaction = new_on_topic
-/datum/expansion/multitool/Destroy()
- interact_predicates.Cut()
- return ..()
-
-/datum/expansion/multitool/proc/interact(var/obj/item/device/multitool/M, var/mob/user)
+/datum/component/multitool/proc/interact(var/obj/item/device/multitool/M, var/mob/user)
if(CanUseTopic(user) != STATUS_INTERACTIVE)
return
- var/html = get_interact_window(M, user)
+ var/html = callback_get_interact_window.Invoke(user, M, src)
if(html)
var/datum/browser/popup = new(usr, "multitool", "Multitool Menu", window_x, window_y)
popup.set_content(html)
@@ -24,13 +21,10 @@
else
close_window(usr)
-/datum/expansion/multitool/proc/get_interact_window(var/obj/item/device/multitool/M, var/mob/user)
- return
-
-/datum/expansion/multitool/proc/close_window(var/mob/user)
+/datum/component/multitool/proc/close_window(var/mob/user)
user << browse(null, "window=multitool")
-/datum/expansion/multitool/proc/buffer(var/obj/item/device/multitool/multitool)
+/datum/component/multitool/proc/buffer(var/obj/item/device/multitool/multitool)
. += "Buffer Memory:
"
var/buffer_name = multitool.get_buffer_name()
if(buffer_name)
@@ -38,7 +32,7 @@
else
. += "No connection stored in the buffer."
-/datum/expansion/multitool/CanUseTopic(var/mob/user)
+/datum/component/multitool/CanUseTopic(var/mob/user)
. = ..()
if(. == STATUS_CLOSE)
return
@@ -46,13 +40,10 @@
if(!user.get_multitool())
return STATUS_CLOSE
- if(!all_predicates_true(list(holder, user), interact_predicates))
- return STATUS_CLOSE
-
- var/datum/host = holder.ui_host()
+ var/datum/host = parent.ui_host()
return user.default_can_use_topic(host)
-/datum/expansion/multitool/Topic(href, href_list)
+/datum/component/multitool/Topic(href, href_list)
if(..())
close_window(usr)
return 1
@@ -66,7 +57,7 @@
M.set_buffer(null)
. = MT_REFRESH
else
- . = on_topic(href, href_list, user)
+ . = on_topic_interaction.Invoke(href, href_list, user, src)
switch(.)
if(MT_REFRESH)
@@ -75,15 +66,15 @@
close_window(user)
return 1
-/datum/expansion/multitool/proc/on_topic(href, href_list, user)
+/datum/component/multitool/proc/on_topic(href, href_list, user)
return MT_NOACTION
-/datum/expansion/multitool/proc/send_buffer(var/obj/item/device/multitool/M, var/atom/buffer, var/mob/user)
+/datum/component/multitool/proc/send_buffer(var/obj/item/device/multitool/M, var/atom/buffer, var/mob/user)
if(M.get_buffer() == buffer && buffer)
receive_buffer(M, buffer, user)
else if(!buffer)
to_chat(user, "Unable to acquire data from the buffered object. Purging from memory.")
return MT_REFRESH
-/datum/expansion/multitool/proc/receive_buffer(var/obj/item/device/multitool/M, var/atom/buffer, var/mob/user)
+/datum/component/multitool/proc/receive_buffer(var/obj/item/device/multitool/M, var/atom/buffer, var/mob/user)
return
diff --git a/code/datums/datum.dm b/code/datums/datum.dm
index 546d1c6d198..63112019345 100644
--- a/code/datums/datum.dm
+++ b/code/datums/datum.dm
@@ -4,6 +4,18 @@
var/tmp/isprocessing = 0
var/tmp/gcDestroyed //Time when this object was destroyed.
+ /// 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
+ /// 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
+
// Default implementation of clean-up code.
// This should be overridden to remove all references pointing to the object being destroyed.
// Return the appropriate QDEL_HINT; in most cases this is QDEL_HINT_QUEUE.
@@ -24,6 +36,37 @@
continue
qdel(timer)
+ // Handle components & signals
+ signal_enabled = FALSE
+
+ var/list/dc = datum_components
+ if(dc)
+ var/all_components = dc[/datum/component]
+ if(length(all_components))
+ for(var/I in all_components)
+ var/datum/component/C = I
+ qdel(C, FALSE, TRUE)
+ else
+ var/datum/component/C = all_components
+ qdel(C, FALSE, TRUE)
+ dc.Cut()
+
+ var/list/lookup = comp_lookup
+ if(lookup)
+ for(var/sig in lookup)
+ var/list/comps = lookup[sig]
+ if(length(comps))
+ for(var/i in comps)
+ var/datum/component/comp = i
+ comp.UnregisterSignal(src, sig)
+ else
+ var/datum/component/comp = comps
+ comp.UnregisterSignal(src, sig)
+ comp_lookup = lookup = null
+
+ for(var/target in signal_procs)
+ UnregisterSignal(target, signal_procs[target])
+
return QDEL_HINT_QUEUE
/datum/proc/process()
diff --git a/code/datums/elements/_element.dm b/code/datums/elements/_element.dm
new file mode 100644
index 00000000000..3fc7c6181a2
--- /dev/null
+++ b/code/datums/elements/_element.dm
@@ -0,0 +1,50 @@
+/**
+ * A holder for simple behaviour that can be attached to many different types
+ *
+ * Only one element of each type is instanced during game init.
+ * Otherwise acts basically like a lightweight component.
+ */
+/datum/element
+ /// Option flags for element behaviour
+ var/element_flags = NONE
+ /**
+ * The index of the first attach argument to consider for duplicate elements
+ *
+ * Is only used when flags contains [ELEMENT_BESPOKE]
+ *
+ * This is infinity so you must explicitly set this
+ */
+ var/id_arg_index = INFINITY
+
+/// Activates the functionality defined by the element on the given target datum
+/datum/element/proc/Attach(datum/target)
+ SHOULD_CALL_PARENT(1)
+ if(type == /datum/element)
+ return ELEMENT_INCOMPATIBLE
+ if(element_flags & ELEMENT_DETACH)
+ RegisterSignal(target, COMSIG_PARENT_QDELETING, .proc/Detach, override = TRUE)
+
+/// Deactivates the functionality defines by the element on the given datum
+/datum/element/proc/Detach(datum/source, force)
+ SHOULD_CALL_PARENT(1)
+ UnregisterSignal(source, COMSIG_PARENT_QDELETING)
+
+/datum/element/Destroy(force)
+ if(!force)
+ return QDEL_HINT_LETMELIVE
+ SSdcs.elements_by_type -= type
+ return ..()
+
+//DATUM PROCS
+
+/// Finds the singleton for the element type given and attaches it to src
+/datum/proc/_AddElement(eletype, ...)
+ var/datum/element/ele = SSdcs.GetElement(eletype)
+ args[1] = src
+ if(ele.Attach(arglist(args)) == ELEMENT_INCOMPATIBLE)
+ CRASH("Incompatible [eletype] assigned to a [type]! args: [json_encode(args)]")
+
+/// Finds the singleton for the element type given and detaches it from src
+/datum/proc/_RemoveElement(eletype)
+ var/datum/element/ele = SSdcs.GetElement(eletype)
+ ele.Detach(src)
diff --git a/code/datums/expansions/expansion.dm b/code/datums/expansions/expansion.dm
deleted file mode 100644
index 7655ffdfaea..00000000000
--- a/code/datums/expansions/expansion.dm
+++ /dev/null
@@ -1,40 +0,0 @@
-/datum/expansion
- var/datum/holder = null // The holder
-
-/datum/expansion/New(var/datum/holder)
- if(!istype(holder))
- CRASH("Invalid holder.")
- src.holder = holder
- ..()
-
-/datum/expansion/Destroy()
- holder = null
- return ..()
-
-/datum/expansion/CanUseTopic(var/mob/user)
- return holder && user ? STATUS_INTERACTIVE : STATUS_CLOSE
-
-/datum/expansion/Topic()
- if(..())
- return 1
- if(CanUseTopic(usr) != STATUS_INTERACTIVE)
- return 1
- return 0
-
-/obj
- var/list/datum/expansion/expansions
-
-/obj/Destroy()
- if (LAZYLEN(expansions))
- for(var/expansion in expansions)
- qdel(expansions[expansion])
-
- LAZYCLEARLIST(expansions)
- expansions = null
- return ..()
-
-/obj/proc/set_expansion(var/type, var/instance)
- LAZYINITLIST(expansions)
- if(expansions[type])
- qdel(expansions[type])
- expansions[type] = instance
diff --git a/code/datums/expansions/multitool/circuitboards/stationalert.dm b/code/datums/expansions/multitool/circuitboards/stationalert.dm
deleted file mode 100644
index d1b061c7c18..00000000000
--- a/code/datums/expansions/multitool/circuitboards/stationalert.dm
+++ /dev/null
@@ -1,29 +0,0 @@
-/datum/expansion/multitool/circuitboards/stationalert/get_interact_window(var/obj/item/device/multitool/M, var/mob/user)
- var/obj/item/circuitboard/stationalert/SA = holder
- . += "Alarm Sources
"
- . += ""
- for(var/datum/alarm_handler/AH in SSalarm.all_handlers)
- . += ""
- . += "| [AH.category] | "
- if(AH in SA.alarm_handlers)
- . += "◼Active | Inactivate | "
- else
- . += "◼Inactive | Activate | "
- . += "
"
- . += "
"
-
-/datum/expansion/multitool/circuitboards/stationalert/on_topic(href, href_list, user)
- var/obj/item/circuitboard/stationalert/SA = holder
- if(href_list["add"])
- var/datum/alarm_handler/AH = locate(href_list["add"]) in SSalarm.all_handlers
- if(AH)
- SA.alarm_handlers |= AH
- return MT_REFRESH
-
- if(href_list["remove"])
- var/datum/alarm_handler/AH = locate(href_list["remove"]) in SSalarm.all_handlers
- if(AH)
- SA.alarm_handlers -= AH
- return MT_REFRESH
-
- return ..()
diff --git a/code/datums/expansions/multitool/items/cable.dm b/code/datums/expansions/multitool/items/cable.dm
deleted file mode 100644
index 69f5e0d9b49..00000000000
--- a/code/datums/expansions/multitool/items/cable.dm
+++ /dev/null
@@ -1,25 +0,0 @@
-/obj/item/stack/cable_coil/Initialize()
- set_expansion(/datum/expansion/multitool, new/datum/expansion/multitool/items/cable(src))
- . = ..()
-
-/datum/expansion/multitool/items/cable/get_interact_window(var/obj/item/device/multitool/M, var/mob/user)
- var/obj/item/stack/cable_coil/cable_coil = holder
- . += "Available Colors
"
- . += ""
- for(var/cable_color in possible_cable_coil_colours)
- . += ""
- . += "| [cable_color] | "
- if(cable_coil.color == possible_cable_coil_colours[cable_color])
- . += "Selected | "
- else
- . += "Select | "
- . += "
"
- . += "
"
-
-/datum/expansion/multitool/items/cable/on_topic(href, href_list, user)
- var/obj/item/stack/cable_coil/cable_coil = holder
- if(href_list["select_color"] && (href_list["select_color"] in possible_cable_coil_colours))
- cable_coil.set_cable_color(href_list["select_color"], user)
- return MT_REFRESH
-
- return ..()
diff --git a/code/datums/expansions/multitool/items/items.dm b/code/datums/expansions/multitool/items/items.dm
deleted file mode 100644
index 54cc971ccf6..00000000000
--- a/code/datums/expansions/multitool/items/items.dm
+++ /dev/null
@@ -1,5 +0,0 @@
-/datum/expansion/multitool/items/CanUseTopic(var/mob/user)
- if(isAI(user)) // No remote AI access
- return STATUS_CLOSE
-
- return ..()
diff --git a/code/datums/expansions/multitool/machinery/cloning.dm b/code/datums/expansions/multitool/machinery/cloning.dm
deleted file mode 100644
index f810b27047a..00000000000
--- a/code/datums/expansions/multitool/machinery/cloning.dm
+++ /dev/null
@@ -1,28 +0,0 @@
-/datum/expansion/multitool/cryo/get_interact_window(var/obj/item/device/multitool/M, var/mob/user)
- . += buffer(M)
- . += "
Connected Cloning Pods:
"
- var/obj/machinery/computer/cloning/C = holder
- for(var/atom/cloning_pod in C.pods)
- . += "[cloning_pod.name]
"
-
-/datum/expansion/multitool/cryo/receive_buffer(var/obj/item/device/multitool/M, var/atom/buffer, var/mob/user)
- var/obj/machinery/clonepod/P = buffer
- var/obj/machinery/computer/cloning/C = holder
-
- if(!istype(P))
- to_chat(user, "No valid connection data in \the [M] buffer.")
- return MT_NOACTION
-
- var/is_connected = (P in C.pods)
- if(!is_connected)
- if(C.connect_pod(P))
- to_chat(user, "You connect \the [P] to \the [C].")
- else
- to_chat(user, "You failed to connect \the [P] to \the [C].")
- return MT_REFRESH
-
- if(C.release_pod(P))
- to_chat(user, "You disconnect \the [P] from \the [C].")
- else
- to_chat(user, "You failed to disconnect \the [P] from \the [C].")
- return MT_REFRESH
diff --git a/code/datums/expansions/multitool/store.dm b/code/datums/expansions/multitool/store.dm
deleted file mode 100644
index 14e8080fa70..00000000000
--- a/code/datums/expansions/multitool/store.dm
+++ /dev/null
@@ -1,10 +0,0 @@
-/datum/expansion/multitool/store/interact(var/obj/item/device/multitool/M, var/mob/user)
- if(CanUseTopic(user) != STATUS_INTERACTIVE)
- return
-
- if(M.get_buffer() == holder)
- M.set_buffer(null)
- to_chat(user, "You purge the connection data of \the [holder] from \the [M].")
- else
- M.set_buffer(holder)
- to_chat(user, "You load connection data from \the [holder] to \the [M].")
diff --git a/code/game/machinery/cloning.dm b/code/game/machinery/cloning.dm
index f464d65be5e..7a3745af301 100644
--- a/code/game/machinery/cloning.dm
+++ b/code/game/machinery/cloning.dm
@@ -54,7 +54,6 @@
/obj/machinery/clonepod/Initialize()
. = ..()
update_icon()
- set_expansion(/datum/expansion/multitool, new/datum/expansion/multitool/store(src))
/obj/machinery/clonepod/Destroy()
if(connected)
diff --git a/code/game/machinery/computer/cloning.dm b/code/game/machinery/computer/cloning.dm
index 10546225f49..213f69e959a 100644
--- a/code/game/machinery/computer/cloning.dm
+++ b/code/game/machinery/computer/cloning.dm
@@ -17,7 +17,6 @@
/obj/machinery/computer/cloning/Initialize()
. = ..()
- set_expansion(/datum/expansion/multitool, new/datum/expansion/multitool/cryo(src, list(/proc/is_operable)))
updatemodules()
/obj/machinery/computer/cloning/Destroy()
diff --git a/code/game/objects/items/devices/multitool.dm b/code/game/objects/items/devices/multitool.dm
index 5b3210feed7..e5acff3217c 100644
--- a/code/game/objects/items/devices/multitool.dm
+++ b/code/game/objects/items/devices/multitool.dm
@@ -72,7 +72,7 @@
return ..(A, user, click_parameters)
var/obj/O = A
- var/datum/expansion/multitool/MT = LAZYACCESS(O.expansions, /datum/expansion/multitool)
+ var/datum/component/multitool/MT = O.GetComponent(/datum/component/multitool)
if(!MT)
return ..(A, user, click_parameters)
diff --git a/code/game/objects/items/weapons/circuitboards/computer/station_alert.dm b/code/game/objects/items/weapons/circuitboards/computer/station_alert.dm
index 7c29f9a63f0..ee890302538 100644
--- a/code/game/objects/items/weapons/circuitboards/computer/station_alert.dm
+++ b/code/game/objects/items/weapons/circuitboards/computer/station_alert.dm
@@ -5,7 +5,6 @@
/obj/item/circuitboard/stationalert/New()
alarm_handlers = new()
- set_expansion(/datum/expansion/multitool, new/datum/expansion/multitool/circuitboards/stationalert(src))
..()
/obj/item/circuitboard/stationalert/construct(var/obj/machinery/computer/station_alert/SA)
diff --git a/code/modules/power/cable.dm b/code/modules/power/cable.dm
index 62fcece8e27..010ef81e5a8 100644
--- a/code/modules/power/cable.dm
+++ b/code/modules/power/cable.dm
@@ -608,6 +608,15 @@ obj/structure/cable/proc/cableColor(var/colorC)
icon_state = "coil"
name = "cable coil"
+/obj/item/stack/cable_coil/attackby(var/obj/item/W, var/mob/user)
+ if(W.ismultitool())
+ choose_cable_color(user)
+ return ..()
+
+/obj/item/stack/cable_coil/proc/choose_cable_color(var/user)
+ var/selected_type = input("Pick new colour.", "Cable Colour", null, null) as null|anything in possible_cable_coil_colours
+ set_cable_color(selected_type, usr)
+
/obj/item/stack/cable_coil/proc/set_cable_color(var/selected_color, var/user)
if(!selected_color)
return
@@ -668,8 +677,7 @@ obj/structure/cable/proc/cableColor(var/colorC)
set name = "Change Colour"
set category = "Object"
- var/selected_type = input("Pick new colour.", "Cable Colour", null, null) as null|anything in possible_cable_coil_colours
- set_cable_color(selected_type, usr)
+ choose_cable_color(usr)
// Items usable on a cable coil :
// - Wirecutters : cut them duh !
diff --git a/code/unit_tests/component_tests.dm b/code/unit_tests/component_tests.dm
new file mode 100644
index 00000000000..0099d7508c5
--- /dev/null
+++ b/code/unit_tests/component_tests.dm
@@ -0,0 +1,12 @@
+/datum/unit_test/component_duping/Run()
+ var/list/bad_dms = list()
+ var/list/bad_dts = list()
+ for(var/t in typesof(/datum/component))
+ var/datum/component/comp = t
+ if(!isnum(initial(comp.dupe_mode)))
+ bad_dms += t
+ var/dupe_type = initial(comp.dupe_type)
+ if(dupe_type && !ispath(dupe_type))
+ bad_dts += t
+ if(length(bad_dms) || length(bad_dts))
+ Fail("Components with invalid dupe modes: ([bad_dms.Join(",")]) ||| Components with invalid dupe types: ([bad_dts.Join(",")])")
diff --git a/html/changelogs/adds_components.yml b/html/changelogs/adds_components.yml
new file mode 100644
index 00000000000..5741ab0679d
--- /dev/null
+++ b/html/changelogs/adds_components.yml
@@ -0,0 +1,6 @@
+author: mikomyazaki, MattAtlas
+
+delete-after: True
+
+changes:
+ - rscadd: "Ports components system from tg, no player-facing changes."