From a4c8ef2d824a3468ee28da8b1143c08d66fa8045 Mon Sep 17 00:00:00 2001 From: Lucy Date: Sat, 21 Mar 2026 21:38:56 -0400 Subject: [PATCH 1/7] Signboards! --- code/datums/components/seethrough.dm | 12 +- code/modules/unit_tests/unit_test.dm | 4 + modular_doppler/signboards/code/_signboard.dm | 224 ++++++++++++++++ modular_doppler/signboards/code/crafting.dm | 23 ++ modular_doppler/signboards/code/holosign.dm | 243 ++++++++++++++++++ .../signboards/icons/signboards.dmi | Bin 0 -> 1986 bytes modular_doppler/signboards/readme.md | 33 +++ tgstation.dme | 3 + 8 files changed, 539 insertions(+), 3 deletions(-) create mode 100644 modular_doppler/signboards/code/_signboard.dm create mode 100644 modular_doppler/signboards/code/crafting.dm create mode 100644 modular_doppler/signboards/code/holosign.dm create mode 100644 modular_doppler/signboards/icons/signboards.dmi create mode 100644 modular_doppler/signboards/readme.md diff --git a/code/datums/components/seethrough.dm b/code/datums/components/seethrough.dm index ba674f67471d7c..6e1f5d8bac763b 100644 --- a/code/datums/components/seethrough.dm +++ b/code/datums/components/seethrough.dm @@ -15,9 +15,13 @@ var/perimeter_reset_timer ///Does this object let clicks from players its transparent to pass through it var/clickthrough + // DOPPLER EDIT ADDITION START - SIGNBOARDS + /// Whether to always use the final turf of the parent as the "effective" parent for calculating coords. + var/use_parent_turf + // DOPPLER EDIT EDITION END ///see_through_map is a define pointing to a specific map. It's basically defining the area which is considered behind. See see_through_maps.dm for a list of maps -/datum/component/seethrough/Initialize(see_through_map = SEE_THROUGH_MAP_DEFAULT, target_alpha = 100, animation_time = 0.5 SECONDS, perimeter_reset_timer = 2 SECONDS, clickthrough = TRUE) +/datum/component/seethrough/Initialize(see_through_map = SEE_THROUGH_MAP_DEFAULT, target_alpha = 100, animation_time = 0.5 SECONDS, perimeter_reset_timer = 2 SECONDS, clickthrough = TRUE, use_parent_turf = FALSE, movement_source = null) // DOPPLER EDIT CHANGE - SIGNBOARDS - ORIGINAL: /datum/component/seethrough/Initialize(see_through_map = SEE_THROUGH_MAP_DEFAULT, target_alpha = 100, animation_time = 0.5 SECONDS, perimeter_reset_timer = 2 SECONDS, clickthrough = TRUE) . = ..() relative_turf_coords = GLOB.see_through_maps[see_through_map] @@ -31,8 +35,9 @@ src.animation_time = animation_time src.perimeter_reset_timer = perimeter_reset_timer src.clickthrough = clickthrough + src.use_parent_turf = use_parent_turf // DOPPLER EDIT ADDITION - SIGNBOARDS - RegisterSignal(parent, COMSIG_MOVABLE_MOVED, PROC_REF(dismantle_perimeter)) + RegisterSignal(movement_source || parent, COMSIG_MOVABLE_MOVED, PROC_REF(dismantle_perimeter)) // DOPPLER EDIT CHANGE - SIGNBOARDS - ORIGINAL: RegisterSignal(parent, COMSIG_MOVABLE_MOVED, PROC_REF(dismantle_perimeter)) setup_perimeter(parent) @@ -40,8 +45,9 @@ /datum/component/seethrough/proc/setup_perimeter(atom/parent) watched_turfs = list() + var/atom/effective_parent = use_parent_turf ? get_turf(parent) : parent // DOPPLER EDIT ADDITION - SIGNBOARDS for(var/list/coordinates as anything in relative_turf_coords) - var/turf/target = TURF_FROM_COORDS_LIST(list(parent.x + coordinates[1], parent.y + coordinates[2], parent.z + coordinates[3])) + var/turf/target = TURF_FROM_COORDS_LIST(list(effective_parent.x + coordinates[1], effective_parent.y + coordinates[2], effective_parent.z + coordinates[3])) // DOPPLER EDIT ADDITION - SIGNBOARDS - ORIGINAL: var/turf/target = TURF_FROM_COORDS_LIST(list(parent.x + coordinates[1], parent.y + coordinates[2], parent.z + coordinates[3])) if(isnull(target)) continue diff --git a/code/modules/unit_tests/unit_test.dm b/code/modules/unit_tests/unit_test.dm index be08e4f82ae315..8341dd492425c4 100644 --- a/code/modules/unit_tests/unit_test.dm +++ b/code/modules/unit_tests/unit_test.dm @@ -360,6 +360,10 @@ GLOBAL_VAR_INIT(focused_tests, focused_tests()) returnable_list += typesof(/turf/open/openspace) returnable_list += typesof(/obj/item/robot_model) // These should never be spawned outside of a robot. + // DOPPLER EDIT ADDITION START - SIGNBOARDS + returnable_list += /obj/effect/abstract/signboard_holder // shouldn't exist outside of signboards + // DOPPLER EDIT ADDITION END + return returnable_list /proc/RunUnitTests() diff --git a/modular_doppler/signboards/code/_signboard.dm b/modular_doppler/signboards/code/_signboard.dm new file mode 100644 index 00000000000000..60d1012baa64e3 --- /dev/null +++ b/modular_doppler/signboards/code/_signboard.dm @@ -0,0 +1,224 @@ +#define INVESTIGATE_SIGNBOARD "signboard" +#define SIGNBOARD_WIDTH (ICON_SIZE_X * 3.5) +#define SIGNBOARD_HEIGHT (ICON_SIZE_Y * 2.5) + +/obj/structure/signboard + name = "sign" + desc = "A foldable sign." + icon = 'modular_doppler/signboards/icons/signboards.dmi' + icon_state = "sign" + base_icon_state = "sign" + density = TRUE + anchored = TRUE + interaction_flags_atom = INTERACT_ATOM_ATTACK_HAND | INTERACT_ATOM_REQUIRES_DEXTERITY + custom_materials = list(/datum/material/wood = SHEET_MATERIAL_AMOUNT * 5) + /// 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 + +/obj/structure/signboard/Initialize(mapload) + . = ..() + text_holder = new(src) + vis_contents += text_holder + 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() + 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/click_alt_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 ITEM_INTERACT_SUCCESS + +/obj/structure/signboard/on_changed_z_level(turf/old_turf, turf/new_turf, same_z_layer, notify_contents) + if(!same_z_layer) + SET_PLANE_EXPLICIT(text_holder, ABOVE_GAME_PLANE, src) + return ..() + +/obj/structure/signboard/set_anchored(anchorvalue) + . = ..() + update_text() + +/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/update_text() + PROTECTED_PROC(TRUE) + if(!should_display_text()) + text_holder.maptext = null + return + var/bwidth = src.bound_width || ICON_SIZE_X + var/bheight = src.bound_height || ICON_SIZE_Y + var/text_html = MAPTEXT_GRAND9K("[html_encode(sign_text)]") + SET_PLANE_EXPLICIT(text_holder, ABOVE_GAME_PLANE, src) + text_holder.layer = ABOVE_ALL_MOB_LAYER + text_holder.alpha = 192 + text_holder.maptext = text_html + text_holder.maptext_x = (SIGNBOARD_WIDTH - bwidth) * -0.5 + text_holder.maptext_y = bheight + text_holder.maptext_width = SIGNBOARD_WIDTH + text_holder.maptext_height = SIGNBOARD_HEIGHT + +/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 + sign_text = trim(new_text, max_length) + update_text() + update_appearance() + +/obj/effect/abstract/signboard_holder + name = "" + icon = null + appearance_flags = APPEARANCE_UI_IGNORE_ALPHA | KEEP_APART + mouse_opacity = MOUSE_OPACITY_TRANSPARENT + +/obj/effect/abstract/signboard_holder/Initialize(mapload) + . = ..() + if(!istype(loc, /obj/structure/signboard) || QDELING(loc)) + return INITIALIZE_HINT_QDEL + AddComponent(/datum/component/seethrough, SEE_THROUGH_MAP_THREE_X_TWO, 112, use_parent_turf = TRUE, movement_source = loc) + +/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/modular_doppler/signboards/code/crafting.dm b/modular_doppler/signboards/code/crafting.dm new file mode 100644 index 00000000000000..6d405f47a250a1 --- /dev/null +++ b/modular_doppler/signboards/code/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_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_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/modular_doppler/signboards/code/holosign.dm b/modular_doppler/signboards/code/holosign.dm new file mode 100644 index 00000000000000..31911d8fdc00f9 --- /dev/null +++ b/modular_doppler/signboards/code/holosign.dm @@ -0,0 +1,243 @@ +/obj/structure/signboard/holosign + name = "holographic sign" + desc = "A holographic signboard, projecting text above it." + icon_state = "holographic_sign" + base_icon_state = "holographic_sign" + layer = ABOVE_MOB_LAYER + density = FALSE + edit_by_hand = TRUE + show_while_unanchored = TRUE + light_range = MINIMUM_USEFUL_LIGHT_RANGE + light_power = 0.3 + light_color = COLOR_CARP_TEAL + light_on = FALSE + custom_materials = list(/datum/material/iron = SHEET_MATERIAL_AMOUNT * 5.05, /datum/material/glass = SMALL_MATERIAL_AMOUNT * 0.7) + /// 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) + . = ..() + text_holder.appearance_flags &= ~RESET_COLOR // allow the text holoder to inherit our color + if(current_color) + INVOKE_ASYNC(src, PROC_REF(set_color), current_color) + AddComponent(/datum/component/usb_port, list( + /obj/item/circuit_component/holo_signboard, + )) + +/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/usb_cable)) + context[SCREENTIP_CONTEXT_LMB] = "Connect USB Cable" + 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/examine(mob/user) + . = ..() + if(obj_flags & EMAGGED) + . += span_warning("
Its locking mechanisms appear to be shorted out!") + else if(registered_owner) + . += 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/set_text(new_text, force) + . = ..() + set_light(l_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 = input(user, "Set Sign Color", full_capitalize(name), current_color) as 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) + testing("before sanitizing: [color]") + . = sanitize_hexcolor(color) + testing("sanitized color: [. || "(null)"]") + if(!. || . == "#000000") + return null + +/obj/structure/signboard/holosign/proc/set_color(new_color) + new_color = sanitize_color(new_color) + if(!new_color) + testing("signboard color invalid or null, removing") + current_color = null + remove_atom_colour(FIXED_COLOUR_PRIORITY) + else + testing("signboard color valid, adding") + current_color = new_color + add_atom_colour(new_color, FIXED_COLOUR_PRIORITY) + set_light(l_color = current_color || src::light_color) + update_appearance() + +/obj/item/circuit_component/holo_signboard + display_name = "Holographic Signboard" + desc = "Output text to a signboard, insert
in the message field to linebreak. Set the color to 0, 0, 0 to reset to default." + circuit_flags = CIRCUIT_FLAG_INPUT_SIGNAL|CIRCUIT_FLAG_OUTPUT_SIGNAL + + var/datum/port/input/message + var/datum/port/input/clear + + var/datum/port/output/fail_reason + var/datum/port/output/on_fail + + var/datum/port/input/red + var/datum/port/input/green + var/datum/port/input/blue + var/datum/port/input/set_color + + var/obj/structure/signboard/holosign/connected_display + +/obj/item/circuit_component/holo_signboard/populate_ports() + message = add_input_port("Message", PORT_TYPE_STRING) + clear = add_input_port("Clear", PORT_TYPE_SIGNAL, trigger = PROC_REF(clear_received)) + + fail_reason = add_output_port("Fail Reason", PORT_TYPE_STRING) + on_fail = add_output_port("Failed", PORT_TYPE_SIGNAL) + + + red = add_input_port("Red", PORT_TYPE_NUMBER) + green = add_input_port("Green", PORT_TYPE_NUMBER) + blue = add_input_port("Blue", PORT_TYPE_NUMBER) + set_color = add_input_port("Set Color", PORT_TYPE_SIGNAL, trigger = PROC_REF(color_received)) + +/obj/item/circuit_component/holo_signboard/register_usb_parent(atom/movable/shell) + . = ..() + if(istype(shell, /obj/structure/signboard/holosign)) + connected_display = shell + +/obj/item/circuit_component/holo_signboard/input_received(datum/port/input/port) + if(!connected_display) + return + if(length(message.value) > connected_display.max_length) //5000 is a hell of a lot longer than 144. + fail_reason.set_output("Too long ([length(message.value)]/[connected_display.max_length]).") + on_fail.set_output(COMPONENT_SIGNAL) + return + if(is_ic_filtered(message.value)) + fail_reason.set_output("Prohibited content.") + on_fail.set_output(COMPONENT_SIGNAL) + return + + var/edited_message = replacetextEx_char(message.value, "
", "\n") + if(connected_display.set_text(edited_message)) + investigate_log("Circuit USB ([parent.get_creator()]) set text to \"[connected_display.sign_text || "(none)"]\"", INVESTIGATE_SIGNBOARD) + if(is_soft_ic_filtered(message.value)) + message_admins("A circuit component (by [parent.get_creator_admin()]) added a soft filtered message to a signboard. [ADMIN_COORDJMP(src)]") + else + fail_reason.set_output("Connection refused by external endpoint.") + on_fail.set_output(COMPONENT_SIGNAL) + +/obj/item/circuit_component/holo_signboard/proc/clear_received(datum/port/input/port) + if(!connected_display.set_text(null)) + fail_reason.set_output("Connection refused by external endpoint.") + on_fail.set_output(COMPONENT_SIGNAL) + +/obj/item/circuit_component/holo_signboard/proc/color_received(datum/port/input/port) + red.set_value(clamp(red.value, 0, 255)) + blue.set_value(clamp(blue.value, 0, 255)) + green.set_value(clamp(green.value, 0, 255)) + var/signboard_color = rgb(red.value, green.value, blue.value) + testing("color_recieved: signboard color: [signboard_color]") + if(signboard_color && signboard_color != rgb(0, 0, 0) && is_color_dark_with_saturation(signboard_color, 25)) + fail_reason.set_output("Color too dark to display.") + on_fail.set_output(COMPONENT_SIGNAL) + return + connected_display.set_color(signboard_color) //doesnt have a return so no need to check and error + investigate_log("Circuit USB ([parent.get_creator()]) set the color to [signboard_color || "(none)"]", INVESTIGATE_SIGNBOARD) + +/// 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 diff --git a/modular_doppler/signboards/icons/signboards.dmi b/modular_doppler/signboards/icons/signboards.dmi new file mode 100644 index 0000000000000000000000000000000000000000..c1db06ee9b7092deb2855003b20fad3241488572 GIT binary patch literal 1986 zcmV;z2R-004jp0{{R3ySV=-0C=3Okg*DbFcd}S&|i?KuSqb*l7trjzJt)A#Zrsg zyYTMeaAP>~zX;*wl609+;~?aEjMviEjS;qA?fk8_%Dgec z_J8_+z@)>;x5D6-tj*;%+wP6HmFVy5#gJDSWMx60p0@S^35`eB0004xP)t-s0000m z;kl=$rvL!B6ciLM000gS4o>czDfHO1w6y>K|IYva&j0-I@bJ#}|Lg1PvH$$%=jWyO z|KH!=-rnAi_~O>q*2mn?vDoX*&dyi+?8wN-jN{c^`_#wB$5;E=#>U22`_xza)WyZc zO#AG_#KcJZ?2ps`!^6W&`^~|@!A$$ecH`B+z`%Cn!A|qnzP`S1*!;Y_yg2Q$u&}U5 z?Q^ZItsd>!tgNg!?QFoSaP0 zW1E|sEYF~xj=`9im@LVUlarH-j-ro`j|<7Vj*gB^o92v+j6|xKiHV6GymN$vgw1F@ zU4+emfPjB+id}?qc6N59VPti6by;EmaBy&0Ve4*gZdqZ-BamZiYHDv`WMyS#3yP0q zWMo-ka$R9$U|?WbVPsfgWS&JiUS3{eRb_fGMK?}LMn* ziC}qwg*#ypZ~;f-?jsH4nQe`sciDvH9Lfq{_zf&;02Ocy2it!lPy0U6Q8ur`vU!dQ zD33HT&FB(PM!+$6tTTOs@$OfE3NGCPOOptgi_IHMK#?QUWi%EV2L(r8!_p-J94^2S zI36-H^dI#fX50@|Y%E}%Wime?&Y09|4tKp+AX`;hQQgts8+LEeJe z0`?*C6|gP9TM)PCLDK=G2dMyW0WQFM(6r!xxCdzq@~50_$9yl=-H`Vn7qBzzc0=t^ zB4B3N?S_0W<^p^-SZq861DX!37+zx|f-T8|Lc34c^mxzEJp^ zqXM%%L<$-ir?lgzOn^A0YU_L z{Kf@}E71Pyrq;aRJO6 z6-*&_HBo8;ga{Cr0Edk)b)66Va3J7tkK{M~DD{34|lHoDb9v@bAIk z^MPDITfn~sA%W)utw8F*fb)TLh8?ZP=e-2O&j+f2*^ccD@3`Z69uG<2`9LeMuL9w_ zp#+={^bi=_Xyw1qj(h~Uz-1^N0z7_W<4Y+4Sv~yxuQfYl6QDobpC7^^Z`r)qF~&4o zU)(Gbc;IzRG22{)%m0Q@$oQEtFyoWDG0^dOo!>v3&i8%13-HC42;KMp?ZGyp_MjE8#l2WFr~;`6>t26}EnwelqaANARsnh;kKY(Pe&Yfh zsWSiL@T;ZD{ErLpSr22d?kjnGHKn^({tZD=L8>fmB UQZJbcd;kCd07*qoM6N<$f|pCsF8}}l literal 0 HcmV?d00001 diff --git a/modular_doppler/signboards/readme.md b/modular_doppler/signboards/readme.md new file mode 100644 index 00000000000000..54d6470b12e345 --- /dev/null +++ b/modular_doppler/signboards/readme.md @@ -0,0 +1,33 @@ +## Signboards + +Module ID: SIGNBOARDS + +### Description: + +Adds signboards! + +### TG Proc/File Changes: + +- `code/modules/unit_tests/unit_test.dm` + - `/datum/unit_test/proc/build_list_of_uncreatables` +- `code/datums/components/seethrough.dm` + - new var: `var/use_parent_turf` + - `/datum/component/seethrough/Initialize` (new args: `use_parent_turf`, `movement_source`) + - `/datum/component/seethrough/proc/setup_perimeter` + +### Modular Overrides: + +- N/A + +### Defines: + +- N/A + +### Included files that are not contained in this module: + +- N/A + +### Credits: + +Absolucy +ancient-engineer (sprites) diff --git a/tgstation.dme b/tgstation.dme index c79ace1716333c..84e70ae0d1a892 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -7743,6 +7743,9 @@ #include "modular_doppler\ships_r_us\code\shuttle_templates\incomplete.dm" #include "modular_doppler\ships_r_us\code\shuttle_templates\mining.dm" #include "modular_doppler\ships_r_us\code\shuttle_templates\pods.dm" +#include "modular_doppler\signboards\code\_signboard.dm" +#include "modular_doppler\signboards\code\crafting.dm" +#include "modular_doppler\signboards\code\holosign.dm" #include "modular_doppler\skillchips\generic_skillchips\misc.dm" #include "modular_doppler\soulcatcher\code\attachable_soulcatcher.dm" #include "modular_doppler\soulcatcher\code\handheld_soulcatcher.dm" From a27f5ddeaa706c55ab60420eb863f7811eb5f7ca Mon Sep 17 00:00:00 2001 From: Lucy Date: Sat, 21 Mar 2026 21:42:10 -0400 Subject: [PATCH 2/7] thats a change, not addition, whoops --- code/datums/components/seethrough.dm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/code/datums/components/seethrough.dm b/code/datums/components/seethrough.dm index 6e1f5d8bac763b..be52c55b7c2f39 100644 --- a/code/datums/components/seethrough.dm +++ b/code/datums/components/seethrough.dm @@ -47,7 +47,7 @@ var/atom/effective_parent = use_parent_turf ? get_turf(parent) : parent // DOPPLER EDIT ADDITION - SIGNBOARDS for(var/list/coordinates as anything in relative_turf_coords) - var/turf/target = TURF_FROM_COORDS_LIST(list(effective_parent.x + coordinates[1], effective_parent.y + coordinates[2], effective_parent.z + coordinates[3])) // DOPPLER EDIT ADDITION - SIGNBOARDS - ORIGINAL: var/turf/target = TURF_FROM_COORDS_LIST(list(parent.x + coordinates[1], parent.y + coordinates[2], parent.z + coordinates[3])) + var/turf/target = TURF_FROM_COORDS_LIST(list(effective_parent.x + coordinates[1], effective_parent.y + coordinates[2], effective_parent.z + coordinates[3])) // DOPPLER EDIT CHANGE - SIGNBOARDS - ORIGINAL: var/turf/target = TURF_FROM_COORDS_LIST(list(parent.x + coordinates[1], parent.y + coordinates[2], parent.z + coordinates[3])) if(isnull(target)) continue From 1c7302c14c658720633924ff652e6d5f0c898ea0 Mon Sep 17 00:00:00 2001 From: Lucy Date: Sun, 22 Mar 2026 18:25:30 -0400 Subject: [PATCH 3/7] remove debug messages --- modular_doppler/signboards/code/holosign.dm | 5 ----- 1 file changed, 5 deletions(-) diff --git a/modular_doppler/signboards/code/holosign.dm b/modular_doppler/signboards/code/holosign.dm index 31911d8fdc00f9..747da93e9f5f50 100644 --- a/modular_doppler/signboards/code/holosign.dm +++ b/modular_doppler/signboards/code/holosign.dm @@ -142,20 +142,16 @@ update_appearance() /obj/structure/signboard/holosign/proc/sanitize_color(color) - testing("before sanitizing: [color]") . = sanitize_hexcolor(color) - testing("sanitized color: [. || "(null)"]") if(!. || . == "#000000") return null /obj/structure/signboard/holosign/proc/set_color(new_color) new_color = sanitize_color(new_color) if(!new_color) - testing("signboard color invalid or null, removing") current_color = null remove_atom_colour(FIXED_COLOUR_PRIORITY) else - testing("signboard color valid, adding") current_color = new_color add_atom_colour(new_color, FIXED_COLOUR_PRIORITY) set_light(l_color = current_color || src::light_color) @@ -228,7 +224,6 @@ blue.set_value(clamp(blue.value, 0, 255)) green.set_value(clamp(green.value, 0, 255)) var/signboard_color = rgb(red.value, green.value, blue.value) - testing("color_recieved: signboard color: [signboard_color]") if(signboard_color && signboard_color != rgb(0, 0, 0) && is_color_dark_with_saturation(signboard_color, 25)) fail_reason.set_output("Color too dark to display.") on_fail.set_output(COMPONENT_SIGNAL) From 5e4456ed36e2c9395c9fed463d641d01fb2467c6 Mon Sep 17 00:00:00 2001 From: Lucy Date: Sun, 22 Mar 2026 18:27:59 -0400 Subject: [PATCH 4/7] use `item_interaction` instead of `attackby` --- modular_doppler/signboards/code/_signboard.dm | 13 ++++++++----- modular_doppler/signboards/code/holosign.dm | 11 ++++++----- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/modular_doppler/signboards/code/_signboard.dm b/modular_doppler/signboards/code/_signboard.dm index 60d1012baa64e3..9279ecc7307e49 100644 --- a/modular_doppler/signboards/code/_signboard.dm +++ b/modular_doppler/signboards/code/_signboard.dm @@ -46,7 +46,7 @@ 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)) + if((edit_by_hand || IS_WRITING_UTENSIL(held_item)) && (anchored || show_while_unanchored)) context[SCREENTIP_CONTEXT_LMB] = "Set Displayed Text" if(sign_text) context[SCREENTIP_CONTEXT_ALT_RMB] = "Clear Sign" @@ -78,10 +78,13 @@ 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/item_interaction(mob/living/user, obj/item/tool, list/modifiers) + if(!IS_WRITING_UTENSIL(tool)) + return NONE + if(try_set_text(user)) + return ITEM_INTERACT_SUCCESS + else + return ITEM_INTERACT_BLOCKING /obj/structure/signboard/attack_hand(mob/living/user, list/modifiers) . = ..() diff --git a/modular_doppler/signboards/code/holosign.dm b/modular_doppler/signboards/code/holosign.dm index 747da93e9f5f50..b0698189006300 100644 --- a/modular_doppler/signboards/code/holosign.dm +++ b/modular_doppler/signboards/code/holosign.dm @@ -67,17 +67,17 @@ 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)) +/obj/structure/signboard/holosign/item_interaction(mob/living/user, obj/item/tool, list/modifiers) + var/obj/item/card/id/id = tool?.GetID() + if(!istype(id)) return ..() var/trimmed_id_name = trimtext(id.registered_name) if(!trimmed_id_name) balloon_alert(user, "no name on id!") - return + return ITEM_INTERACT_BLOCKING if(obj_flags & EMAGGED) balloon_alert(user, "lock shorted out!") - return + return ITEM_INTERACT_BLOCKING if(registered_owner) if(!check_locked(user)) registered_owner = null @@ -88,6 +88,7 @@ balloon_alert(user, "locked to id") investigate_log("([key_name(user)]) added id lock for \"[registered_owner]\"", INVESTIGATE_SIGNBOARD) update_appearance() + return ITEM_INTERACT_SUCCESS /obj/structure/signboard/holosign/is_locked(mob/living/user) . = ..() From 4647f8cd115fb8b401afeca0baf6e484d06027e8 Mon Sep 17 00:00:00 2001 From: Lucy Date: Sun, 22 Mar 2026 18:32:02 -0400 Subject: [PATCH 5/7] pen stuff --- modular_doppler/signboards/code/_signboard.dm | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/modular_doppler/signboards/code/_signboard.dm b/modular_doppler/signboards/code/_signboard.dm index 9279ecc7307e49..4d5cbc4fb4a696 100644 --- a/modular_doppler/signboards/code/_signboard.dm +++ b/modular_doppler/signboards/code/_signboard.dm @@ -90,7 +90,7 @@ . = ..() if(.) return - if(!edit_by_hand && !user.is_holding_item_of_type(/obj/item/pen)) + if(!edit_by_hand && !is_holding_pen(user)) balloon_alert(user, "need a pen!") return TRUE if(try_set_text(user)) @@ -132,7 +132,7 @@ . = ..() 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)) + if(!edit_by_hand && !is_holding_pen(user)) balloon_alert(user, "need a pen!") return if(check_locked(user)) @@ -201,6 +201,12 @@ update_text() update_appearance() +/obj/structure/signboard/proc/is_holding_pen(mob/living/user) + for(var/obj/item/item in user.held_items) + if(IS_WRITING_UTENSIL(item)) + return TRUE + return FALSE + /obj/effect/abstract/signboard_holder name = "" icon = null From d270b9b757ee32a1fa3aeac747297624da8c0f4d Mon Sep 17 00:00:00 2001 From: Lucy Date: Sun, 22 Mar 2026 22:18:20 -0400 Subject: [PATCH 6/7] Clean up holosign context code a bit --- modular_doppler/signboards/code/holosign.dm | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/modular_doppler/signboards/code/holosign.dm b/modular_doppler/signboards/code/holosign.dm index b0698189006300..f630c22990d633 100644 --- a/modular_doppler/signboards/code/holosign.dm +++ b/modular_doppler/signboards/code/holosign.dm @@ -29,18 +29,17 @@ /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/usb_cable)) + if(is_locked(user)) + return + if(istype(held_item, /obj/item/usb_cable)) context[SCREENTIP_CONTEXT_LMB] = "Connect USB Cable" - else if(!locked && istype(held_item?.GetID(), /obj/item/card/id)) + else if(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 + context[SCREENTIP_CONTEXT_RMB] = "Set Sign Color" + return CONTEXTUAL_SCREENTIP_SET /obj/structure/signboard/holosign/update_icon_state() base_icon_state = current_color ? "[initial(base_icon_state)]_greyscale" : initial(base_icon_state) From df5cb72ab715b115b0543d439658525ff37820f4 Mon Sep 17 00:00:00 2001 From: Lucy Date: Sun, 22 Mar 2026 22:19:23 -0400 Subject: [PATCH 7/7] I honestly forgot why those were `INVOKE_ASYNC`, but they prolly don't need to be at all. --- modular_doppler/signboards/code/holosign.dm | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/modular_doppler/signboards/code/holosign.dm b/modular_doppler/signboards/code/holosign.dm index f630c22990d633..51d66abe3ad6a2 100644 --- a/modular_doppler/signboards/code/holosign.dm +++ b/modular_doppler/signboards/code/holosign.dm @@ -22,7 +22,7 @@ . = ..() text_holder.appearance_flags &= ~RESET_COLOR // allow the text holoder to inherit our color if(current_color) - INVOKE_ASYNC(src, PROC_REF(set_color), current_color) + set_color(current_color) AddComponent(/datum/component/usb_port, list( /obj/item/circuit_component/holo_signboard, )) @@ -61,7 +61,7 @@ /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) + set_color(var_value) datum_flags |= DF_VAR_EDITED return TRUE return ..() @@ -122,7 +122,7 @@ return if(check_locked(user)) return - INVOKE_ASYNC(src, PROC_REF(set_color), new_color) + 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)