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"