diff --git a/code/__DEFINES/logging.dm b/code/__DEFINES/logging.dm
index 172f2cdf4682..3660cc6ebf3a 100644
--- a/code/__DEFINES/logging.dm
+++ b/code/__DEFINES/logging.dm
@@ -24,6 +24,7 @@
#define INVESTIGATE_WIRES "wires"
#define INVESTIGATE_NANITES "nanites"
#define INVESTIGATE_ARTIFACT "artifact"
+#define INVESTIGATE_SIGNBOARD "signboard" // monkestation addition
// Logging types for log_message()
#define LOG_ATTACK (1 << 0)
diff --git a/code/__DEFINES/~monkestation/_helpers.dm b/code/__DEFINES/~monkestation/_helpers.dm
new file mode 100644
index 000000000000..d2234e5852ee
--- /dev/null
+++ b/code/__DEFINES/~monkestation/_helpers.dm
@@ -0,0 +1,8 @@
+/// Basically, this is UNTIL(Condition),
+/// but it also checks to see if Src has been qdeleted, and returns if so.
+#define UNTIL_WHILE_EXISTS(Src, Condition) \
+ while(!(Condition)) { \
+ if(QDELETED(Src)) return; \
+ stoplag(); \
+ } \
+ if(QDELETED(Src)) return;
diff --git a/code/__HELPERS/~monkestation-helpers/colors.dm b/code/__HELPERS/~monkestation-helpers/colors.dm
new file mode 100644
index 000000000000..40c4e5e93526
--- /dev/null
+++ b/code/__HELPERS/~monkestation-helpers/colors.dm
@@ -0,0 +1,19 @@
+/// Given a color in the format of "#RRGGBB", will return if the color
+/// is dark. Value is mixed with Saturation and Brightness from HSV.
+/proc/is_color_dark_with_saturation(color, threshold = 25)
+ var/hsl = rgb2num(color, COLORSPACE_HSL)
+ return hsl[3] < threshold
+
+/// it checks if a color is dark, but without saturation value.
+/// This uses Brightness only, without Saturation from HSV
+/proc/is_color_dark_without_saturation(color, threshold = 25)
+ return get_color_brightness_from_hex(color) < threshold
+
+/// returns HSV brightness 0 to 100 by color hex
+/proc/get_color_brightness_from_hex(A)
+ if(!A || length(A) != length_char(A))
+ return 0
+ var/R = hex2num(copytext(A, 2, 4))
+ var/G = hex2num(copytext(A, 4, 6))
+ var/B = hex2num(copytext(A, 6, 8))
+ return round(max(R, G, B)/2.55, 1)
diff --git a/code/modules/admin/admin_investigate.dm b/code/modules/admin/admin_investigate.dm
index 3f4c041f387a..406add5b76b9 100644
--- a/code/modules/admin/admin_investigate.dm
+++ b/code/modules/admin/admin_investigate.dm
@@ -35,6 +35,7 @@
INVESTIGATE_RESEARCH,
INVESTIGATE_WIRES,
INVESTIGATE_NANITES,
+ INVESTIGATE_SIGNBOARD, // monkestation addition
)
var/list/logs_present = list("notes, memos, watchlist")
diff --git a/code/modules/unit_tests/unit_test.dm b/code/modules/unit_tests/unit_test.dm
index 8647b026adc4..e3446e24618a 100644
--- a/code/modules/unit_tests/unit_test.dm
+++ b/code/modules/unit_tests/unit_test.dm
@@ -254,6 +254,7 @@ GLOBAL_VAR_INIT(focused_tests, focused_tests())
/obj/machinery/ocean_elevator,
/atom/movable/outdoor_effect,
/turf/closed/mineral/random/regrowth,
+ /obj/effect/abstract/signboard_holder, // monkestation addition: shouldn't exist outside of signboards
)
//Say it with me now, type template
ignore += typesof(/obj/effect/mapping_helpers)
diff --git a/monkestation/code/modules/blueshift/structures/wooden_rack.dm b/monkestation/code/modules/blueshift/structures/wooden_rack.dm
index e279f5520f75..0e624b64309e 100644
--- a/monkestation/code/modules/blueshift/structures/wooden_rack.dm
+++ b/monkestation/code/modules/blueshift/structures/wooden_rack.dm
@@ -141,6 +141,7 @@ GLOBAL_LIST_INIT(monke_wood_recipes, list(
new/datum/stack_recipe("sturdy wooden fence", /obj/structure/railing/wooden_fencing, 5, time = 2 SECONDS, one_per_turf = TRUE, on_solid_ground = TRUE, category = CAT_STRUCTURE),
new/datum/stack_recipe("sturdy wooden fence gate", /obj/structure/railing/wooden_fencing/gate, 5, time = 2 SECONDS, one_per_turf = TRUE, on_solid_ground = TRUE, category = CAT_STRUCTURE),
new/datum/stack_recipe("large wooden gate", /obj/structure/mineral_door/wood/large_gate, 10, time = 5 SECONDS, one_per_turf = TRUE, on_solid_ground = TRUE, category = CAT_STRUCTURE),
+ new/datum/stack_recipe("signboard", /obj/structure/signboard, 5, time = 5 SECONDS, one_per_turf = TRUE, on_solid_ground = TRUE, category = CAT_FURNITURE),
))
diff --git a/monkestation/code/modules/signboards/_signboard.dm b/monkestation/code/modules/signboards/_signboard.dm
new file mode 100644
index 000000000000..e37b88eee22e
--- /dev/null
+++ b/monkestation/code/modules/signboards/_signboard.dm
@@ -0,0 +1,309 @@
+#define SIGNBOARD_WIDTH (world.icon_size * 3.5)
+#define SIGNBOARD_HEIGHT (world.icon_size * 2.5)
+
+/obj/structure/signboard
+ name = "sign"
+ desc = "A foldable sign."
+ icon = 'monkestation/icons/obj/structures/signboards.dmi'
+ icon_state = "sign"
+ base_icon_state = "sign"
+ density = TRUE
+ anchored = TRUE
+ interaction_flags_atom = INTERACT_ATOM_ATTACK_HAND | INTERACT_ATOM_REQUIRES_DEXTERITY
+ /// The current text written on the sign.
+ var/sign_text
+ /// The maximum length of text that can be input onto the sign.
+ var/max_length = MAX_PLAQUE_LEN
+ /// If true, the text cannot be changed by players.
+ var/locked = FALSE
+ /// If text should be shown while unanchored.
+ var/show_while_unanchored = FALSE
+ /// If TRUE, the sign can be edited without a pen.
+ var/edit_by_hand = FALSE
+ /// Holder for signboard maptext
+ var/obj/effect/abstract/signboard_holder/text_holder
+ /// Lazy assoc list of clients to images
+ VAR_PROTECTED/list/client_maptext_images
+ /// If a mass client add/removal is currently being done.
+ VAR_PRIVATE/doing_update = FALSE
+
+/obj/structure/signboard/Initialize(mapload)
+ . = ..()
+ text_holder = new(src)
+ vis_contents += text_holder
+ RegisterSignal(SSdcs, COMSIG_GLOB_MOB_LOGGED_IN, PROC_REF(on_mob_login))
+ if(sign_text)
+ set_text(sign_text, force = TRUE)
+ investigate_log("had its text set on load to \"[sign_text]\"", INVESTIGATE_SIGNBOARD)
+ update_appearance()
+ register_context()
+
+/obj/structure/signboard/Destroy()
+ UnregisterSignal(SSdcs, COMSIG_GLOB_MOB_LOGGED_IN)
+ remove_from_all_clients_unsafe()
+ vis_contents -= text_holder
+ QDEL_NULL(text_holder)
+ return ..()
+
+/obj/structure/signboard/add_context(atom/source, list/context, obj/item/held_item, mob/user)
+ . = ..()
+ if(!is_locked(user))
+ if(held_item?.tool_behaviour == TOOL_WRENCH)
+ context[SCREENTIP_CONTEXT_LMB] = anchored ? "Unsecure" : "Secure"
+ return CONTEXTUAL_SCREENTIP_SET
+ if((edit_by_hand || istype(held_item, /obj/item/pen)) && (anchored || show_while_unanchored))
+ context[SCREENTIP_CONTEXT_LMB] = "Set Displayed Text"
+ if(sign_text)
+ context[SCREENTIP_CONTEXT_ALT_RMB] = "Clear Sign"
+ return CONTEXTUAL_SCREENTIP_SET
+
+/obj/structure/signboard/examine(mob/user)
+ . = ..()
+ if(!edit_by_hand)
+ . += span_info("You need a pen to write on the sign!")
+ if(anchored)
+ . += span_info("It is secured to the floor, you could use a wrench to unsecure and move it.")
+ else
+ . += span_info("It is unsecured, you could use a wrench to secure it in place.")
+ if(sign_text)
+ . += span_boldnotice("\nIt currently displays the following:")
+ . += span_info(html_encode(sign_text))
+ else
+ . += span_info("\nIt is blank!")
+
+/obj/structure/signboard/update_icon_state()
+ . = ..()
+ icon_state = "[base_icon_state][sign_text ? "" : "_blank"]"
+
+/obj/structure/signboard/vv_edit_var(var_name, var_value)
+ if(var_name == NAMEOF(src, sign_text))
+ if(!set_text(var_value, force = TRUE))
+ return FALSE
+ datum_flags |= DF_VAR_EDITED
+ return TRUE
+ return ..()
+
+/obj/structure/signboard/attackby(obj/item/item, mob/user, params)
+ if(!istype(item, /obj/item/pen))
+ return ..()
+ try_set_text(user)
+
+/obj/structure/signboard/attack_hand(mob/living/user, list/modifiers)
+ . = ..()
+ if(.)
+ return
+ if(!edit_by_hand && !user.is_holding_item_of_type(/obj/item/pen))
+ balloon_alert(user, "need a pen!")
+ return TRUE
+ if(try_set_text(user))
+ return TRUE
+
+/obj/structure/signboard/proc/try_set_text(mob/living/user)
+ . = FALSE
+ if(!anchored && !show_while_unanchored)
+ return FALSE
+ if(check_locked(user))
+ return FALSE
+ var/new_text = tgui_input_text(
+ user,
+ message = "What would you like to set this sign's text to?",
+ title = full_capitalize(name),
+ default = sign_text,
+ max_length = max_length,
+ multiline = TRUE,
+ encode = FALSE
+ )
+ if(QDELETED(src) || !new_text || check_locked(user))
+ return FALSE
+ var/list/filter_result = CAN_BYPASS_FILTER(user) ? null : is_ic_filtered(new_text)
+ if(filter_result)
+ REPORT_CHAT_FILTER_TO_USER(user, filter_result)
+ return FALSE
+ var/list/soft_filter_result = CAN_BYPASS_FILTER(user) ? null : is_soft_ic_filtered(new_text)
+ if(soft_filter_result)
+ if(tgui_alert(user, "Your message contains \"[soft_filter_result[CHAT_FILTER_INDEX_WORD]]\". \"[soft_filter_result[CHAT_FILTER_INDEX_REASON]]\", Are you sure you want to say it?", "Soft Blocked Word", list("Yes", "No")) != "Yes")
+ return FALSE
+ message_admins("[ADMIN_LOOKUPFLW(user)] has passed the soft filter for \"[soft_filter_result[CHAT_FILTER_INDEX_WORD]]\" when writing to the sign at [ADMIN_VERBOSEJMP(src)], they may be using a disallowed term. Sign text: \"[html_encode(new_text)]\"")
+ log_admin_private("[key_name(user)] has passed the soft filter for \"[soft_filter_result[CHAT_FILTER_INDEX_WORD]]\" when writing to the sign at [loc_name(src)], they may be using a disallowed term. Sign text: \"[new_text]\"")
+ if(set_text(new_text))
+ balloon_alert(user, "set text")
+ investigate_log("([key_name(user)]) set text to \"[sign_text || "(none)"]\"", INVESTIGATE_SIGNBOARD)
+ return TRUE
+
+/obj/structure/signboard/alt_click_secondary(mob/user)
+ . = ..()
+ if(!sign_text || !can_interact(user) || !user.can_perform_action(src, NEED_DEXTERITY))
+ return
+ if(!edit_by_hand && !user.is_holding_item_of_type(/obj/item/pen))
+ balloon_alert(user, "need a pen!")
+ return
+ if(check_locked(user))
+ return
+ if(set_text(null))
+ balloon_alert(user, "cleared text")
+ investigate_log("([key_name(user)]) cleared the text", INVESTIGATE_SIGNBOARD)
+
+/obj/structure/signboard/wrench_act(mob/living/user, obj/item/tool)
+ . = ..()
+ if(!anchored || !check_locked(user))
+ default_unfasten_wrench(user, tool)
+ return TOOL_ACT_TOOLTYPE_SUCCESS
+
+/obj/structure/signboard/set_anchored(anchorvalue)
+ . = ..()
+ INVOKE_ASYNC(src, PROC_REF(add_to_all_clients))
+
+/obj/structure/signboard/Moved(atom/old_loc, movement_dir, forced, list/old_locs, momentum_change)
+ . = ..()
+ if(!isturf(old_loc) || !isturf(loc))
+ INVOKE_ASYNC(src, PROC_REF(add_to_all_clients))
+
+/obj/structure/signboard/proc/is_locked(mob/user)
+ . = locked
+ if(isAdminGhostAI(user))
+ return FALSE
+
+/obj/structure/signboard/proc/check_locked(mob/user, silent = FALSE)
+ . = is_locked(user)
+ if(. && !silent)
+ balloon_alert(user, "locked!")
+
+/obj/structure/signboard/proc/should_display_text()
+ if(QDELETED(src) || !isturf(loc) || !sign_text)
+ return FALSE
+ if(!anchored && !show_while_unanchored)
+ return FALSE
+ return TRUE
+
+/obj/structure/signboard/proc/on_mob_login(datum/source, mob/user)
+ SIGNAL_HANDLER
+ var/client/client = user?.client
+ ASYNC
+ UNTIL_WHILE_EXISTS(src, !doing_update)
+ doing_update = TRUE
+ add_client(client)
+ doing_update = FALSE
+
+/obj/structure/signboard/proc/add_client(client/user)
+ if(QDELETED(user) || !should_display_text())
+ return
+ if(LAZYACCESS(client_maptext_images, user))
+ remove_client(user)
+ var/image/client_image = create_image_for_client(user)
+ if(!client_image || QDELETED(user))
+ return
+ LAZYSET(client_maptext_images, user, client_image)
+ LAZYADD(update_on_z, client_image)
+ user.images |= client_image
+ RegisterSignal(user, COMSIG_QDELETING, PROC_REF(remove_client))
+
+/obj/structure/signboard/proc/remove_client(client/user)
+ SIGNAL_HANDLER
+ if(isnull(user))
+ return
+ UnregisterSignal(user, COMSIG_QDELETING)
+ var/image/client_image = LAZYACCESS(client_maptext_images, user)
+ if(!client_image)
+ return
+ user.images -= client_image
+ LAZYREMOVE(client_maptext_images, user)
+ LAZYREMOVE(update_on_z, client_image)
+
+/obj/structure/signboard/proc/add_to_all_clients()
+ UNTIL_WHILE_EXISTS(src, !doing_update)
+ doing_update = TRUE
+ add_to_all_clients_unsafe()
+ doing_update = FALSE
+
+/obj/structure/signboard/proc/add_to_all_clients_unsafe()
+ PRIVATE_PROC(TRUE)
+ if(QDELETED(src))
+ return
+ remove_from_all_clients_unsafe()
+ if(!should_display_text())
+ return
+ var/list/shown_first = list()
+ var/client/usr_client = usr?.client
+ add_client(usr_client)
+ for(var/mob/mob in viewers(world.view, src))
+ if(QDELING(mob) || QDELETED(mob.client) || mob == usr)
+ continue
+ add_client(mob.client)
+ shown_first[mob.client] = TRUE
+ for(var/client/client as anything in GLOB.clients)
+ if(QDELETED(client) || shown_first[client] || client == usr_client)
+ continue
+ add_client(client)
+
+/obj/structure/signboard/proc/remove_from_all_clients()
+ UNTIL_WHILE_EXISTS(src, !doing_update)
+ doing_update = TRUE
+ remove_from_all_clients_unsafe()
+ doing_update = FALSE
+
+/obj/structure/signboard/proc/remove_from_all_clients_unsafe()
+ PRIVATE_PROC(TRUE)
+ for(var/client/client as anything in client_maptext_images)
+ remove_client(client)
+ LAZYNULL(client_maptext_images)
+
+/obj/structure/signboard/proc/create_image_for_client(client/user) as /image
+ RETURN_TYPE(/image)
+ if(QDELETED(user) || !sign_text)
+ return
+ var/bwidth = src.bound_width || world.icon_size
+ var/bheight = src.bound_height || world.icon_size
+ var/text_html = MAPTEXT_GRAND9K("[html_encode(sign_text)]")
+ var/mheight
+ WXH_TO_HEIGHT(user.MeasureText(text_html, null, SIGNBOARD_WIDTH), mheight)
+ var/image/maptext_holder = image(loc = text_holder)
+ SET_PLANE_EXPLICIT(maptext_holder, GAME_PLANE_UPPER_FOV_HIDDEN, src)
+ maptext_holder.layer = ABOVE_ALL_MOB_LAYER
+ maptext_holder.appearance_flags = APPEARANCE_UI_IGNORE_ALPHA | KEEP_APART
+ maptext_holder.alpha = 192
+ maptext_holder.maptext = text_html
+ maptext_holder.maptext_x = (SIGNBOARD_WIDTH - bwidth) * -0.5
+ maptext_holder.maptext_y = bheight
+ maptext_holder.maptext_width = SIGNBOARD_WIDTH
+ maptext_holder.maptext_height = mheight
+ return maptext_holder
+
+/obj/structure/signboard/proc/set_text(new_text, force = FALSE)
+ . = FALSE
+ if(QDELETED(src) || (locked && !force))
+ return
+ if(!istext(new_text) && !isnull(new_text))
+ CRASH("Attempted to set invalid signtext: [new_text]")
+ . = TRUE
+ new_text = trimtext(copytext_char(new_text, 1, max_length))
+ if(length(new_text))
+ sign_text = new_text
+ INVOKE_ASYNC(src, PROC_REF(add_to_all_clients))
+ else
+ sign_text = null
+ INVOKE_ASYNC(src, PROC_REF(remove_from_all_clients))
+ update_appearance()
+
+/obj/effect/abstract/signboard_holder
+ name = ""
+ icon = null
+ mouse_opacity = MOUSE_OPACITY_TRANSPARENT
+ vis_flags = VIS_INHERIT_PLANE
+
+/obj/effect/abstract/signboard_holder/Initialize(mapload)
+ . = ..()
+ if(!istype(loc, /obj/structure/signboard) || QDELING(loc))
+ return INITIALIZE_HINT_QDEL
+
+/obj/effect/abstract/signboard_holder/Destroy(force)
+ if(!force && istype(loc, /obj/structure/signboard) && !QDELING(loc))
+ stack_trace("Tried to delete a signboard holder that's inside of a non-deleted signboard!")
+ return QDEL_HINT_LETMELIVE
+ return ..()
+
+/obj/effect/abstract/signboard_holder/forceMove(atom/destination, no_tp = FALSE, harderforce = FALSE)
+ if(harderforce)
+ return ..()
+
+#undef SIGNBOARD_HEIGHT
+#undef SIGNBOARD_WIDTH
diff --git a/monkestation/code/modules/signboards/crafting.dm b/monkestation/code/modules/signboards/crafting.dm
new file mode 100644
index 000000000000..80989c8b30f0
--- /dev/null
+++ b/monkestation/code/modules/signboards/crafting.dm
@@ -0,0 +1,23 @@
+/datum/crafting_recipe/signboard
+ name = "Signboard"
+ desc = "A sign, you can write anything on it!"
+ tool_behaviors = list(TOOL_WRENCH, TOOL_SCREWDRIVER)
+ result = /obj/structure/signboard
+ reqs = list(
+ /obj/item/stack/sheet/mineral/wood = 5,
+ )
+ time = 5 SECONDS
+ category = CAT_FURNITURE
+
+/datum/crafting_recipe/holosign
+ name = "Holographic Signboard"
+ desc = "A sign, you can write anything on it! Now available in many colors!"
+ tool_behaviors = list(TOOL_WRENCH, TOOL_SCREWDRIVER, TOOL_MULTITOOL)
+ result = /obj/structure/signboard/holosign
+ reqs = list(
+ /obj/item/stack/sheet/iron = 5,
+ /obj/item/stack/cable_coil = 5,
+ /obj/item/stock_parts/micro_laser = 1,
+ )
+ time = 10 SECONDS
+ category = CAT_FURNITURE
diff --git a/monkestation/code/modules/signboards/holosign.dm b/monkestation/code/modules/signboards/holosign.dm
new file mode 100644
index 000000000000..a7133b5de9af
--- /dev/null
+++ b/monkestation/code/modules/signboards/holosign.dm
@@ -0,0 +1,163 @@
+/obj/structure/signboard/holosign
+ name = "holographic sign"
+ desc = "A holographic signboard, projecting text above it."
+ icon_state = "holographic_sign"
+ base_icon_state = "holographic_sign"
+ edit_by_hand = TRUE
+ show_while_unanchored = TRUE
+ light_system = OVERLAY_LIGHT
+ light_outer_range = MINIMUM_USEFUL_LIGHT_RANGE
+ light_power = 0.3
+ light_color = COLOR_CARP_TEAL
+ light_on = FALSE
+ /// If set, only IDs with this name can (un)lock the sign.
+ var/registered_owner
+ /// The current color of the sign.
+ /// The sign will be greyscale if this is set.
+ var/current_color
+
+/obj/structure/signboard/holosign/Initialize(mapload)
+ . = ..()
+ if(current_color)
+ INVOKE_ASYNC(src, PROC_REF(set_color), current_color)
+
+/obj/structure/signboard/holosign/add_context(atom/source, list/context, obj/item/held_item, mob/user)
+ . = ..()
+ var/locked = is_locked(user)
+ if(istype(held_item, /obj/item/card/emag))
+ context[SCREENTIP_CONTEXT_LMB] = "Short Out Locking Mechanisms"
+ . = CONTEXTUAL_SCREENTIP_SET
+ else if(!locked && istype(held_item?.GetID(), /obj/item/card/id))
+ context[SCREENTIP_CONTEXT_LMB] = registered_owner ? "Remove ID Lock" : "Lock To ID"
+ . = CONTEXTUAL_SCREENTIP_SET
+ if(!locked)
+ context[SCREENTIP_CONTEXT_RMB] = "Set Sign Color"
+ . = CONTEXTUAL_SCREENTIP_SET
+
+/obj/structure/signboard/holosign/update_icon_state()
+ base_icon_state = current_color ? "[initial(base_icon_state)]_greyscale" : initial(base_icon_state)
+ . = ..()
+ if(obj_flags & EMAGGED)
+ icon_state += "_emag"
+
+/obj/structure/signboard/holosign/update_desc(updates)
+ . = ..()
+ desc = initial(desc)
+ if(obj_flags & EMAGGED)
+ desc += span_warning("
Its locking mechanisms appear to be shorted out!")
+ else if(registered_owner)
+ desc += span_info("
It is locked to the ID of [span_name(registered_owner)].")
+
+/obj/structure/signboard/holosign/update_overlays()
+ . = ..()
+ if(sign_text)
+ . += emissive_appearance(icon, "holographic_sign_e", src)
+
+/obj/structure/signboard/holosign/vv_edit_var(var_name, var_value)
+ if(var_name == NAMEOF(src, color) || var_name == NAMEOF(src, current_color))
+ INVOKE_ASYNC(src, PROC_REF(set_color), var_value)
+ datum_flags |= DF_VAR_EDITED
+ return TRUE
+ return ..()
+
+/obj/structure/signboard/holosign/attackby(obj/item/item, mob/user, params)
+ var/obj/item/card/id/id = item?.GetID()
+ if(!istype(id) || !can_interact(user) || !user.can_perform_action(src, NEED_DEXTERITY))
+ return ..()
+ var/trimmed_id_name = trimtext(id.registered_name)
+ if(!trimmed_id_name)
+ balloon_alert(user, "no name on id!")
+ return
+ if(obj_flags & EMAGGED)
+ balloon_alert(user, "lock shorted out!")
+ return
+ if(registered_owner)
+ if(!check_locked(user))
+ registered_owner = null
+ balloon_alert(user, "id lock removed")
+ investigate_log("([key_name(user)]) removed id lock", INVESTIGATE_SIGNBOARD)
+ else
+ registered_owner = trimmed_id_name
+ balloon_alert(user, "locked to id")
+ investigate_log("([key_name(user)]) added id lock for \"[registered_owner]\"", INVESTIGATE_SIGNBOARD)
+ update_appearance()
+
+/obj/structure/signboard/holosign/is_locked(mob/living/user)
+ . = ..()
+ if(.)
+ return
+ if(registered_owner && isliving(user))
+ var/obj/item/card/id/id = user.get_idcard()
+ if(!istype(id) || QDELING(id))
+ return TRUE
+ return !cmptext(trimtext(id.registered_name), registered_owner)
+
+/obj/structure/signboard/holosign/create_image_for_client(client/user)
+ RETURN_TYPE(/image)
+ var/image/client_image = ..()
+ if(current_color)
+ client_image?.color = current_color
+ return client_image
+
+/obj/structure/signboard/holosign/set_text(new_text, force)
+ . = ..()
+ set_light_on(!!sign_text)
+
+/obj/structure/signboard/holosign/attack_hand_secondary(mob/user, list/modifiers)
+ . = ..()
+ if(. == SECONDARY_ATTACK_CANCEL_ATTACK_CHAIN)
+ return
+ if(try_set_color(user))
+ return SECONDARY_ATTACK_CANCEL_ATTACK_CHAIN
+
+/obj/structure/signboard/holosign/proc/try_set_color(mob/user)
+ . = TRUE
+ if(!can_interact(user) || !user.can_perform_action(src, NEED_DEXTERITY))
+ return FALSE
+ if(check_locked(user))
+ return
+ var/new_color = sanitize_color(tgui_color_picker(user, "Set Sign Color", full_capitalize(name), current_color))
+ if(new_color && is_color_dark_with_saturation(new_color, 25))
+ balloon_alert(user, "color too dark!")
+ return
+ if(check_locked(user))
+ return
+ INVOKE_ASYNC(src, PROC_REF(set_color), new_color)
+ if(new_color)
+ balloon_alert(user, "set color to [new_color]")
+ investigate_log("([key_name(user)]) set the color to [new_color || "(none)"]", INVESTIGATE_SIGNBOARD)
+ else
+ balloon_alert(user, "unset color")
+ investigate_log("([key_name(user)]) cleared the color", INVESTIGATE_SIGNBOARD)
+
+/obj/structure/signboard/holosign/emag_act(mob/user, obj/item/card/emag/emag_card)
+ if(obj_flags & EMAGGED)
+ return FALSE
+ playsound(src, SFX_SPARKS, vol = 100, vary = TRUE, extrarange = SHORT_RANGE_SOUND_EXTRARANGE)
+ do_sparks(3, cardinal_only = FALSE, source = src)
+ balloon_alert(user, "lock broken")
+ investigate_log("was emagged by [key_name(user)] (previous owner: [registered_owner || "(none)"])", INVESTIGATE_SIGNBOARD)
+ registered_owner = null
+ obj_flags |= EMAGGED
+ update_appearance()
+
+/obj/structure/signboard/holosign/proc/sanitize_color(color)
+ . = sanitize_hexcolor(color)
+ if(!. || . == "#000000")
+ return null
+
+/obj/structure/signboard/holosign/proc/set_color(new_color)
+ new_color = sanitize_color(new_color)
+ if(!new_color)
+ current_color = null
+ remove_atom_colour(FIXED_COLOUR_PRIORITY)
+ else
+ current_color = new_color
+ add_atom_colour(new_color, FIXED_COLOUR_PRIORITY)
+ set_light_color(current_color || initial(light_color))
+ for(var/client/client as anything in client_maptext_images)
+ if(QDELETED(client))
+ continue
+ var/image/client_image = client_maptext_images[client]
+ client_image.color = current_color
+ update_appearance()
diff --git a/monkestation/icons/obj/structures/signboards.dmi b/monkestation/icons/obj/structures/signboards.dmi
new file mode 100644
index 000000000000..041afb87c52f
Binary files /dev/null and b/monkestation/icons/obj/structures/signboards.dmi differ
diff --git a/tgstation.dme b/tgstation.dme
index bb345c3db7b4..cbd38f26457f 100644
--- a/tgstation.dme
+++ b/tgstation.dme
@@ -391,6 +391,7 @@
#include "code\__DEFINES\traits\sources.dm"
#include "code\__DEFINES\traits\monkestation\declarations.dm"
#include "code\__DEFINES\traits\monkestation\sources.dm"
+#include "code\__DEFINES\~monkestation\_helpers.dm"
#include "code\__DEFINES\~monkestation\_patreon.dm"
#include "code\__DEFINES\~monkestation\abberant_organs.dm"
#include "code\__DEFINES\~monkestation\access.dm"
@@ -590,6 +591,7 @@
#include "code\__HELPERS\~monkestation-helpers\atoms.dm"
#include "code\__HELPERS\~monkestation-helpers\clients.dm"
#include "code\__HELPERS\~monkestation-helpers\cmp.dm"
+#include "code\__HELPERS\~monkestation-helpers\colors.dm"
#include "code\__HELPERS\~monkestation-helpers\icon_smoothing.dm"
#include "code\__HELPERS\~monkestation-helpers\icons.dm"
#include "code\__HELPERS\~monkestation-helpers\mapping.dm"
@@ -7639,6 +7641,9 @@
#include "monkestation\code\modules\security\code\weapons\lawbringer.dm"
#include "monkestation\code\modules\security\code\weapons\paco.dm"
#include "monkestation\code\modules\shelves\shelf.dm"
+#include "monkestation\code\modules\signboards\_signboard.dm"
+#include "monkestation\code\modules\signboards\crafting.dm"
+#include "monkestation\code\modules\signboards\holosign.dm"
#include "monkestation\code\modules\skyrat_snipes\languages.dm"
#include "monkestation\code\modules\skyrat_snipes\reagents\drink_reagents.dm"
#include "monkestation\code\modules\skyrat_snipes\vending_machines\vending_food.dm"