diff --git a/code/_helpers/unsorted.dm b/code/_helpers/unsorted.dm
index c7f3022b09f..ec102c2258a 100644
--- a/code/_helpers/unsorted.dm
+++ b/code/_helpers/unsorted.dm
@@ -1595,3 +1595,7 @@ GLOBAL_REAL_VAR(list/stack_trace_storage)
/proc/CallAsync(datum/source, proctype, list/arguments)
set waitfor = FALSE
return call(source, proctype)(arglist(arguments))
+
+// call to generate a stack trace and print to runtime logs
+/proc/get_stack_trace(msg, file, line)
+ CRASH("%% [file],[line] %% [msg]")
diff --git a/code/_macros.dm b/code/_macros.dm
index 603b7c45e34..72c63735a2d 100644
--- a/code/_macros.dm
+++ b/code/_macros.dm
@@ -62,3 +62,5 @@
//check if all bitflags specified are present
#define CHECK_MULTIPLE_BITFIELDS(flagvar, flags) ((flagvar & (flags)) == flags)
+
+#define PRINT_STACK_TRACE(X) get_stack_trace(X, __FILE__, __LINE__)
diff --git a/code/datums/extensions/_defines.dm b/code/datums/extensions/_defines.dm
new file mode 100644
index 00000000000..7aa1d04638e
--- /dev/null
+++ b/code/datums/extensions/_defines.dm
@@ -0,0 +1,2 @@
+#define EXTENSION_FLAG_NONE 0
+#define EXTENSION_FLAG_IMMEDIATE 1 // Instantly instantiates, instead of doing it lazily.
diff --git a/code/datums/extensions/event_registration.dm b/code/datums/extensions/event_registration.dm
new file mode 100644
index 00000000000..fc18dcfd86e
--- /dev/null
+++ b/code/datums/extensions/event_registration.dm
@@ -0,0 +1,26 @@
+// For registering for events to be called when certain conditions are met.
+
+/datum/extension/event_registration
+ base_type = /datum/extension/event_registration
+ expected_type = /datum
+ flags = EXTENSION_FLAG_IMMEDIATE
+ var/decl/observ/event
+ var/datum/target
+ var/callproc
+
+/datum/extension/event_registration/New(datum/holder, decl/observ/event, datum/target, callproc)
+ ..()
+ event.register(target, src, .proc/trigger)
+ GLOB.destroyed_event.register(target, src, .proc/qdel_self)
+
+ src.event = event
+ src.target = target
+ src.callproc = callproc
+
+/datum/extension/event_registration/Destroy()
+ GLOB.destroyed_event.unregister(target, src, .proc/qdel_self)
+ event.unregister(target, src)
+ . = ..()
+
+/datum/extension/event_registration/proc/trigger()
+ call(holder, callproc)(arglist(args))
diff --git a/code/datums/extensions/extensions.dm b/code/datums/extensions/extensions.dm
new file mode 100644
index 00000000000..969a4c8fbe7
--- /dev/null
+++ b/code/datums/extensions/extensions.dm
@@ -0,0 +1,77 @@
+/datum/extension
+ var/base_type
+ var/datum/holder = null // The holder
+ var/expected_type = /datum
+ var/flags = EXTENSION_FLAG_NONE
+
+/datum/extension/New(var/datum/holder)
+ if(!istype(holder, expected_type))
+ CRASH("Invalid holder type. Expected [expected_type], was [holder.type]")
+ src.holder = holder
+
+/datum/extension/proc/post_construction()
+
+/datum/extension/Destroy()
+ holder = null
+ . = ..()
+
+/datum
+ var/list/datum/extension/extensions
+
+//Variadic - Additional positional arguments can be given. Named arguments might not work so well
+/proc/set_extension(var/datum/source, var/datum/extension/extension_type)
+ var/datum/extension/extension_base_type = initial(extension_type.base_type)
+ if(!ispath(extension_base_type, /datum/extension))
+ CRASH("Invalid base type: Expected /datum/extension, was [log_info_line(extension_base_type)]")
+ if(!ispath(extension_type, extension_base_type))
+ CRASH("Invalid extension type: Expected [extension_base_type], was [log_info_line(extension_type)]")
+ if(!source.extensions)
+ source.extensions = list()
+ var/datum/extension/existing_extension = source.extensions[extension_base_type]
+ if(istype(existing_extension))
+ qdel(existing_extension)
+
+ if(initial(extension_base_type.flags) & EXTENSION_FLAG_IMMEDIATE)
+ var/datum/extension/created = construct_extension_instance(extension_type, source, args.Copy(3))
+ source.extensions[extension_base_type] = created
+ created.post_construction()
+ return created
+
+ var/list/extension_data = list(extension_type, source)
+ if(args.len > 2)
+ extension_data += args.Copy(3)
+ source.extensions[extension_base_type] = extension_data
+
+/proc/get_or_create_extension(var/datum/source, var/datum/extension/extension_type)
+ var/base_type = initial(extension_type.base_type)
+ if(!has_extension(source, base_type))
+ set_extension(arglist(args))
+ return get_extension(source, base_type)
+
+/proc/get_extension(var/datum/source, var/base_type)
+ if(!source.extensions)
+ return
+ . = source.extensions[base_type]
+ if(!.)
+ return
+ if(islist(.)) //a list, so it's expecting to be lazy-loaded
+ var/list/extension_data = .
+ var/datum/extension/created = construct_extension_instance(extension_data[1], extension_data[2], extension_data.Copy(3))
+ source.extensions[base_type] = created
+ created.post_construction()
+ return created
+
+//Fast way to check if it has an extension, also doesn't trigger instantiation of lazy loaded extensions
+/proc/has_extension(var/datum/source, var/base_type)
+ return !!(source.extensions && source.extensions[base_type])
+
+/proc/construct_extension_instance(var/extension_type, var/datum/source, var/list/arguments)
+ arguments = list(source) + arguments
+ return new extension_type(arglist(arguments))
+
+/proc/remove_extension(var/datum/source, var/base_type)
+ if(!source.extensions || !source.extensions[base_type])
+ return
+ if(!islist(source.extensions[base_type]))
+ qdel(source.extensions[base_type])
+ LAZYREMOVE(source.extensions, base_type)
\ No newline at end of file
diff --git a/code/datums/extensions/interactive.dm b/code/datums/extensions/interactive.dm
new file mode 100644
index 00000000000..c6036f0b2ae
--- /dev/null
+++ b/code/datums/extensions/interactive.dm
@@ -0,0 +1,36 @@
+//Extensions that can be interacted with via Topic
+/datum/extension/interactive
+ base_type = /datum/extension/interactive
+ var/list/host_predicates
+ var/list/user_predicates
+
+/datum/extension/interactive/New(var/datum/holder, var/host_predicates = list(), var/user_predicates = list())
+ ..()
+
+ src.host_predicates = host_predicates ? host_predicates : list()
+ src.user_predicates = user_predicates ? user_predicates : list()
+
+/datum/extension/interactive/Destroy()
+ host_predicates.Cut()
+ user_predicates.Cut()
+ return ..()
+
+/datum/extension/interactive/proc/extension_status(var/mob/user)
+ if(!holder || !user)
+ return STATUS_CLOSE
+ if(!all_predicates_true(list(holder), host_predicates))
+ return STATUS_CLOSE
+ if(!all_predicates_true(list(user), user_predicates))
+ return STATUS_CLOSE
+ if(holder.CanUseTopic(user, global.default_topic_state) != STATUS_INTERACTIVE)
+ return STATUS_CLOSE
+
+ return STATUS_INTERACTIVE
+
+/datum/extension/interactive/proc/extension_act(var/href, var/list/href_list, var/mob/user)
+ return extension_status(user) == STATUS_CLOSE
+
+/datum/extension/interactive/Topic(var/href, var/list/href_list)
+ if(..())
+ return TRUE
+ return extension_act(href, href_list, usr)
\ No newline at end of file
diff --git a/code/datums/extensions/label.dm b/code/datums/extensions/label.dm
new file mode 100644
index 00000000000..8c0ffc490bc
--- /dev/null
+++ b/code/datums/extensions/label.dm
@@ -0,0 +1,112 @@
+/datum/extension/labels
+ base_type = /datum/extension/labels
+ expected_type = /atom
+ var/atom/atom_holder
+ var/list/labels
+
+/datum/extension/labels/New()
+ ..()
+ atom_holder = holder
+
+/datum/extension/labels/Destroy()
+ atom_holder = null
+ return ..()
+
+/datum/extension/labels/proc/AttachLabel(var/mob/user, var/label)
+ if(!CanAttachLabel(user, label))
+ return
+
+ if(!LAZYLEN(labels))
+ atom_holder.verbs += /atom/proc/RemoveLabel
+ LAZYADD(labels, label)
+
+ if(user)
+ user.visible_message("\The [user] attaches a label to \the [atom_holder].", \
+ "You attach a label, '[label]', to \the [atom_holder].")
+
+ var/old_name = atom_holder.name
+ atom_holder.name = "[atom_holder.name] ([label])"
+ var/decl/observ/name_set/N = GET_DECL(/decl/observ/name_set)
+ N.raise_event(src, old_name, atom_holder.name)
+ return TRUE
+
+/datum/extension/labels/proc/RemoveLabel(var/mob/user, var/label)
+ if(!(label in labels))
+ return
+
+ LAZYREMOVE(labels, label)
+ if(!LAZYLEN(labels))
+ atom_holder.verbs -= /atom/proc/RemoveLabel
+
+ var/full_label = " ([label])"
+ var/index = findtextEx(atom_holder.name, full_label)
+ if(!index) // Playing it safe, something might not have set the name properly
+ return
+
+ if(user)
+ user.visible_message("\The [user] removes a label from \the [atom_holder].", \
+ "You remove a label, '[label]', from \the [atom_holder].")
+
+ var/old_name = atom_holder.name
+ // We find and replace the first instance, since that's the one we removed from the list
+ atom_holder.name = replacetext(atom_holder.name, full_label, "", index, index + length(full_label))
+ var/decl/observ/name_set/N = GET_DECL(/decl/observ/name_set)
+ N.raise_event(src, old_name, atom_holder.name)
+ return TRUE
+
+/datum/extension/labels/proc/RemoveAllLabels()
+ . = TRUE
+ for(var/lbl in labels)
+ if(!RemoveLabel(null, lbl))
+ . = FALSE
+
+// We may have to do something more complex here
+// in case something appends strings to something that's labelled rather than replace the name outright
+// Non-printable characters should be of help if this comes up
+/datum/extension/labels/proc/AppendLabelsToName(var/name)
+ if(!LAZYLEN(labels))
+ return name
+ . = list(name)
+ for(var/entry in labels)
+ . += " ([entry])"
+ . = jointext(., null)
+
+/datum/extension/labels/proc/CanAttachLabel(var/user, var/label)
+ if(!length(label))
+ return FALSE
+ if(ExcessLabelLength(label, user))
+ return FALSE
+ return TRUE
+
+/datum/extension/labels/proc/ExcessLabelLength(var/label, var/user)
+ . = length(label) + 3 // Each label also adds a space and two brackets when applied to a name
+ if(LAZYLEN(labels))
+ for(var/entry in labels)
+ . += length(entry) + 3
+ . = . > 64 ? TRUE : FALSE
+ if(. && user)
+ to_chat(user, "The label won't fit.")
+
+/proc/get_attached_labels(var/atom/source)
+ if(has_extension(source, /datum/extension/labels))
+ var/datum/extension/labels/L = get_extension(source, /datum/extension/labels)
+ if(LAZYLEN(L.labels))
+ return L.labels.Copy()
+ return list()
+
+/atom/proc/RemoveLabel(var/label in get_attached_labels(src))
+ set name = "Remove Label"
+ set desc = "Used to remove labels"
+ set category = "Object"
+ set src in view(1)
+
+ if(Adjacent(usr))
+ if(has_extension(src, /datum/extension/labels))
+ var/datum/extension/labels/L = get_extension(src, /datum/extension/labels)
+ L.RemoveLabel(usr, label)
+
+//Single label allowed for this one
+/datum/extension/labels/single/CanAttachLabel(user, label)
+ if(LAZYLEN(labels) >= 1) //Only allow a single label
+ return FALSE
+ . = ..()
diff --git a/code/datums/observation/name_set.dm b/code/datums/observation/name_set.dm
new file mode 100644
index 00000000000..3eab52425a2
--- /dev/null
+++ b/code/datums/observation/name_set.dm
@@ -0,0 +1,28 @@
+// Observer Pattern Implementation: Name Set
+// Registration type: /atom
+//
+// Raised when: An atom's name changes.
+//
+// Arguments that the called proc should expect:
+// /atom/namee: The atom that had its name set
+// /old_name: name before the change
+// /new_name: name after the change
+
+/decl/observ/name_set
+ name = "Name Set"
+ expected_type = /atom
+
+/*********************
+* Name Set Handling *
+*********************/
+
+/atom/proc/SetName(var/new_name)
+ var/old_name = name
+ if(old_name != new_name)
+ name = new_name
+ if(has_extension(src, /datum/extension/labels))
+ var/datum/extension/labels/L = get_extension(src, /datum/extension/labels)
+ name = L.AppendLabelsToName(name)
+
+ var/decl/observ/name_set/N = GET_DECL(/decl/observ/name_set)
+ N.raise_event(src, old_name, name)
diff --git a/code/datums/state_machine/state.dm b/code/datums/state_machine/state.dm
new file mode 100644
index 00000000000..55bb1a93fff
--- /dev/null
+++ b/code/datums/state_machine/state.dm
@@ -0,0 +1,30 @@
+// An individual state, defined as a `/decl` to save memory.
+// On a directed graph, these would be the nodes themselves, connected to each other by unidirectional arrows.
+/decl/state
+ // Transition decl types, which get turned into refs to those types.
+ // Note that the order DOES matter, as decls earlier in the list have higher priority
+ // if more than one becomes 'open'.
+ var/list/transitions = null
+
+/decl/state/Initialize()
+ . = ..()
+ for(var/i in 1 to LAZYLEN(transitions))
+ var/decl/state_transition/T = GET_DECL(transitions[i])
+ T.from += src
+ transitions[i] = T
+
+// Returns a list of transitions that a FSM could switch to.
+// Note that `holder` is NOT the FSM, but instead the thing the FSM is attached to.
+/decl/state/proc/get_open_transitions(datum/holder)
+ for(var/decl/state_transition/T as anything in transitions)
+ if(T.is_open(holder))
+ LAZYADD(., T)
+
+// Stub for child states to modify the holder when switched to.
+// Again, `holder` is not the FSM.
+/decl/state/proc/entered_state(datum/holder)
+ return
+
+// Another stub for when leaving a state.
+/decl/state/proc/exited_state(datum/holder)
+ return
\ No newline at end of file
diff --git a/code/datums/state_machine/transition.dm b/code/datums/state_machine/transition.dm
new file mode 100644
index 00000000000..ade5daf683a
--- /dev/null
+++ b/code/datums/state_machine/transition.dm
@@ -0,0 +1,16 @@
+// Used to connect `/decl/state`s together so the FSM knows what state to switch to, and on what conditions.
+// On a directed graph, these would be the arrows connecting the nodes representing states.
+/decl/state_transition
+ var/list/from = null
+ var/decl/state/target = null
+
+// Called by one or more state decls acting as nodes in a directed graph.
+/decl/state_transition/Initialize()
+ . = ..()
+ LAZYINITLIST(from)
+ if(ispath(target))
+ target = GET_DECL(target)
+
+// Tells the FSM if it should or should not be allowed to transfer to the target state.
+/decl/state_transition/proc/is_open(datum/holder)
+ return FALSE
\ No newline at end of file
diff --git a/code/modules/mob/living/silicon/silicon.dm b/code/modules/mob/living/silicon/silicon.dm
index 1526cc959dd..df5e4470d03 100644
--- a/code/modules/mob/living/silicon/silicon.dm
+++ b/code/modules/mob/living/silicon/silicon.dm
@@ -49,9 +49,10 @@
idcard = new idcard_type(src)
set_id_info(idcard)
-/mob/living/silicon/proc/SetName(pickedName as text)
- real_name = pickedName
+/mob/living/silicon/SetName(new_name as text)
+ real_name = new_name
name = real_name
+ ..()
/mob/living/silicon/proc/show_laws()
return
diff --git a/code/modules/paperwork/handlabeler.dm b/code/modules/paperwork/handlabeler.dm
index 0162fa479f9..f9c68a99049 100644
--- a/code/modules/paperwork/handlabeler.dm
+++ b/code/modules/paperwork/handlabeler.dm
@@ -29,43 +29,53 @@
if(length(A.name) + length(label) > 64)
to_chat(user, SPAN_WARNING("\The [src]'s label too big."))
return
- if(istype(A, /mob/living/silicon/robot/platform))
- var/mob/living/silicon/robot/platform/P = A
- if(!P.allowed(user))
- to_chat(usr, SPAN_WARNING("Access denied."))
- else if(P.client || P.key)
- to_chat(user, SPAN_NOTICE("You rename \the [P] to [label]."))
- to_chat(P, SPAN_NOTICE("\The [user] renames you to [label]."))
- P.custom_name = label
- P.SetName(P.custom_name)
- else
- to_chat(user, SPAN_WARNING("\The [src] is inactive and cannot be renamed."))
- return
- if(ishuman(A))
- to_chat(user, SPAN_WARNING("The label refuses to stick to [A.name]."))
- return
- if(issilicon(A))
- to_chat(user, SPAN_WARNING("The label refuses to stick to [A.name]."))
- return
- if(isobserver(A))
- to_chat(user, SPAN_WARNING("[src] passes through [A.name]."))
+
+ if(has_extension(A, /datum/extension/labels))
+ var/datum/extension/labels/L = get_extension(A, /datum/extension/labels)
+ if(!L.CanAttachLabel(user, label))
+ return
+ A.attach_label(user, src, label)
+
+/atom/proc/attach_label(var/user, var/atom/labeler, var/label_text)
+ to_chat(user, "The label refuses to stick to [name].")
+
+/mob/observer/attach_label(var/user, var/atom/labeler, var/label_text)
+ to_chat(user, "\The [labeler] passes through \the [src].")
+
+/obj/machinery/portable_atmospherics/hydroponics/attach_label(var/user, var/atom/labeler, var/label_text)
+ if(!mechanical)
+ to_chat(user, "How are you going to label that?")
return
- if(istype(A, /obj/item/reagent_containers/glass))
- to_chat(user, SPAN_WARNING("The label can't stick to the [A.name] (Try using a pen)."))
+ ..()
+ update_icon()
+
+/obj/attach_label(var/user, var/atom/labeler, var/label_text)
+ if(!simulated)
return
- if(istype(A, /obj/machinery/portable_atmospherics/hydroponics))
- var/obj/machinery/portable_atmospherics/hydroponics/tray = A
- if(!tray.mechanical)
- to_chat(user, SPAN_WARNING("How are you going to label that?"))
- return
- tray.labelled = label
- spawn(1)
- tray.update_icon()
+ var/datum/extension/labels/L = get_or_create_extension(src, /datum/extension/labels)
+ return L.AttachLabel(user, label_text)
+
+/mob/living/silicon/robot/platform/attach_label(var/user, var/atom/labeler, var/label_text)
+ if(!allowed(user))
+ to_chat(usr, SPAN_WARNING("Access denied."))
+ else if(client || key)
+ to_chat(user, SPAN_NOTICE("You rename \the [src] to [label_text]."))
+ to_chat(src, SPAN_NOTICE("\The [user] renames you to [label_text]."))
+ SetName(label_text)
+ else
+ to_chat(user, SPAN_WARNING("\The [src] is inactive and cannot be renamed."))
- user.visible_message( \
- SPAN_NOTICE("\The [user] labels [A] as [label]."), \
- SPAN_NOTICE("You label [A] as [label]."))
- A.name = "[A.name] ([label])"
+/obj/item/reagent_containers/glass/attach_label(var/user, var/atom/labeler, var/label_text)
+ to_chat(user, SPAN_WARNING("The label can't stick to the [name] (Try using a pen)."))
+ return
+
+/obj/machinery/portable_atmospherics/hydroponics/attach_label(var/user, var/atom/labeler, var/label_text)
+ if(!mechanical)
+ to_chat(user, SPAN_WARNING("How are you going to label that?"))
+ return
+ ..()
+ spawn(1)
+ update_icon()
/obj/item/hand_labeler/attack_self(mob/user as mob)
mode = !mode
@@ -80,4 +90,4 @@
label = str
to_chat(user, SPAN_NOTICE("You set the text to '[str]'."))
else
- to_chat(user, SPAN_NOTICE("You turn off \the [src]."))
\ No newline at end of file
+ to_chat(user, SPAN_NOTICE("You turn off \the [src]."))
diff --git a/code/modules/reagents/machinery/dispenser/cartridge.dm b/code/modules/reagents/machinery/dispenser/cartridge.dm
index f01b8c1f046..9acb6aabd65 100644
--- a/code/modules/reagents/machinery/dispenser/cartridge.dm
+++ b/code/modules/reagents/machinery/dispenser/cartridge.dm
@@ -36,20 +36,23 @@
set category = "Object"
set src in view(usr, 1)
- setLabel(L, usr)
+ var/datum/extension/labels/lext = get_or_create_extension(src, /datum/extension/labels)
+ if(lext)
+ for(var/lab in lext.labels)
+ lext.RemoveLabel(null, lab)
+ if(length(L))
+ lext.AttachLabel(null, L)
/obj/item/reagent_containers/chem_disp_cartridge/proc/setLabel(L, mob/user = null)
- if(L)
- if(user)
- to_chat(user, "You set the label on \the [src] to '[L]'.")
-
- label = L
- name = "[initial(name)] - '[L]'"
- else
- if(user)
- to_chat(user, "You clear the label on \the [src].")
- label = ""
- name = initial(name)
+ var/datum/extension/labels/lext = get_or_create_extension(src, /datum/extension/labels)
+ if(lext)
+ for(var/lab in lext.labels)
+ lext.RemoveLabel(null, lab)
+
+ if(length(L))
+ lext.AttachLabel(user, L)
+ else if(user)
+ to_chat(user, SPAN_NOTICE("You clear the label on \the [src]."))
/obj/item/reagent_containers/chem_disp_cartridge/attack_self()
..()
diff --git a/polaris.dme b/polaris.dme
index 48fd42a51f5..b84fec6fdf7 100644
--- a/polaris.dme
+++ b/polaris.dme
@@ -310,6 +310,10 @@
#include "code\datums\components\crafting\tool_quality.dm"
#include "code\datums\components\crafting\recipes\archery.dm"
#include "code\datums\elements\_element.dm"
+#include "code\datums\extensions\_defines.dm"
+#include "code\datums\extensions\event_registration.dm"
+#include "code\datums\extensions\extensions.dm"
+#include "code\datums\extensions\label.dm"
#include "code\datums\game_masters\_common.dm"
#include "code\datums\game_masters\default.dm"
#include "code\datums\game_masters\other_game_masters.dm"
@@ -344,6 +348,7 @@
#include "code\datums\observation\helpers.dm"
#include "code\datums\observation\logged_in.dm"
#include "code\datums\observation\moved.dm"
+#include "code\datums\observation\name_set.dm"
#include "code\datums\observation\observation.dm"
#include "code\datums\observation\power_change.dm"
#include "code\datums\observation\shuttle_added.dm"
@@ -388,6 +393,8 @@
#include "code\datums\roundstats\_defines_local.dm"
#include "code\datums\roundstats\departmentgoal.dm"
#include "code\datums\roundstats\roundstats.dm"
+#include "code\datums\state_machine\state.dm"
+#include "code\datums\state_machine\transition.dm"
#include "code\datums\supplypacks\atmospherics.dm"
#include "code\datums\supplypacks\contraband.dm"
#include "code\datums\supplypacks\costumes.dm"