From 49940c373e6094e6375968914d4c72778b286518 Mon Sep 17 00:00:00 2001 From: LetterN <24603524+LetterN@users.noreply.github.com> Date: Thu, 28 Oct 2021 12:51:31 +0800 Subject: [PATCH] tgui backend --- code/__DEFINES/colors.dm | 9 + code/__DEFINES/dcs/signals.dm | 2 + code/__HELPERS/_logging.dm | 13 + code/__HELPERS/unsorted.dm | 6 +- code/_globalvars/logging.dm | 3 + code/_onclick/hud/screen_objects.dm | 135 ++-- .../configuration/entries/logging.dm | 4 + code/datums/browser.dm | 103 +-- .../components/storage/concrete/_concrete.dm | 29 +- code/datums/components/storage/storage.dm | 58 +- code/datums/materials/_material.dm | 2 +- code/game/atoms.dm | 12 + code/game/objects/items.dm | 111 ++-- code/modules/client/client_defines.dm | 3 + code/modules/surgery/tools.dm | 4 +- code/modules/tgchat/to_chat.dm | 61 +- code/modules/tgui/external.dm | 1 + code/modules/tgui/status_composers.dm | 111 ++++ code/modules/tgui/tgui.dm | 2 - code/modules/tgui/tgui_input_text.dm | 52 +- code/modules/tgui_panel/external.dm | 4 +- code/modules/tgui_panel/telemetry.dm | 1 + code/modules/tgui_panel/tgui_panel.dm | 1 - code/modules/tooltip/tooltip.dm | 32 +- .../wiremod/components/abstract/compare.dm | 58 ++ .../wiremod/components/abstract/module.dm | 306 +++++++++ .../wiremod/components/action/light.dm | 68 ++ code/modules/wiremod/components/action/mmi.dm | 175 +++++ .../wiremod/components/action/pathfind.dm | 113 ++++ .../modules/wiremod/components/action/pull.dm | 31 + .../wiremod/components/action/radio.dm | 74 +++ .../wiremod/components/action/soundemitter.dm | 66 ++ .../wiremod/components/action/speech.dm | 40 ++ .../wiremod/components/admin/getvar.dm | 42 ++ .../wiremod/components/admin/proccall.dm | 72 +++ code/modules/wiremod/components/admin/sdql.dm | 36 ++ .../wiremod/components/admin/setvar.dm | 36 ++ .../modules/wiremod/components/admin/spawn.dm | 47 ++ .../wiremod/components/admin/to_type.dm | 28 + .../wiremod/components/atom/direction.dm | 66 ++ code/modules/wiremod/components/atom/gps.dm | 34 + .../modules/wiremod/components/atom/health.dm | 62 ++ code/modules/wiremod/components/atom/hear.dm | 35 + code/modules/wiremod/components/atom/self.dm | 21 + .../wiremod/components/atom/species.dm | 35 + .../wiremod/components/hud/bar_overlay.dm | 62 ++ .../wiremod/components/hud/counter_overlay.dm | 103 +++ .../wiremod/components/hud/object_overlay.dm | 122 ++++ .../components/hud/target_intercept.dm | 64 ++ .../modules/wiremod/components/list/concat.dm | 48 ++ .../wiremod/components/list/get_column.dm | 42 ++ code/modules/wiremod/components/list/index.dm | 42 ++ .../wiremod/components/list/index_table.dm | 42 ++ .../wiremod/components/list/list_literal.dm | 86 +++ .../modules/wiremod/components/list/select.dm | 93 +++ code/modules/wiremod/components/list/split.dm | 42 ++ .../wiremod/components/math/arithmetic.dm | 88 +++ .../wiremod/components/math/comparison.dm | 62 ++ .../modules/wiremod/components/math/length.dm | 29 + code/modules/wiremod/components/math/logic.dm | 57 ++ code/modules/wiremod/components/math/not.dm | 29 + .../modules/wiremod/components/math/random.dm | 40 ++ .../wiremod/components/ntnet/ntnet_receive.dm | 62 ++ .../wiremod/components/ntnet/ntnet_send.dm | 29 + .../components/sensors/pressuresensor.dm | 36 ++ .../wiremod/components/sensors/tempsensor.dm | 36 ++ .../wiremod/components/string/concat.dm | 42 ++ .../wiremod/components/string/contains.dm | 35 + .../wiremod/components/string/textcase.dm | 54 ++ .../wiremod/components/string/tonumber.dm | 29 + .../wiremod/components/string/tostring.dm | 40 ++ .../wiremod/components/utility/clock.dm | 57 ++ .../wiremod/components/utility/delay.dm | 43 ++ .../wiremod/components/utility/getter.dm | 70 ++ .../wiremod/components/utility/router.dm | 82 +++ .../wiremod/components/utility/setter.dm | 58 ++ .../wiremod/components/utility/typecast.dm | 60 ++ .../wiremod/components/utility/typecheck.dm | 51 ++ code/modules/wiremod/core/admin_panel.dm | 75 +++ code/modules/wiremod/core/component.dm | 270 ++++++++ .../modules/wiremod/core/component_printer.dm | 394 ++++++++++++ code/modules/wiremod/core/datatypes.dm | 91 +++ code/modules/wiremod/core/duplicator.dm | 227 +++++++ .../wiremod/core/integrated_circuit.dm | 600 ++++++++++++++++++ code/modules/wiremod/core/marker.dm | 60 ++ code/modules/wiremod/core/port.dm | 216 +++++++ code/modules/wiremod/core/usb_cable.dm | 123 ++++ code/modules/wiremod/core/variable.dm | 49 ++ code/modules/wiremod/datatypes/any.dm | 10 + code/modules/wiremod/datatypes/basic.dm | 9 + code/modules/wiremod/datatypes/entity.dm | 10 + code/modules/wiremod/datatypes/number.dm | 14 + code/modules/wiremod/datatypes/option.dm | 33 + code/modules/wiremod/datatypes/signal.dm | 17 + code/modules/wiremod/datatypes/string.dm | 17 + code/modules/wiremod/preset/hello_world.dm | 14 + code/modules/wiremod/preset/speech_relay.dm | 20 + code/modules/wiremod/shell/airlock.dm | 133 ++++ code/modules/wiremod/shell/bot.dm | 55 ++ .../wiremod/shell/brain_computer_interface.dm | 553 ++++++++++++++++ code/modules/wiremod/shell/compact_remote.dm | 51 ++ code/modules/wiremod/shell/controller.dm | 89 +++ code/modules/wiremod/shell/drone.dm | 82 +++ code/modules/wiremod/shell/moneybot.dm | 140 ++++ code/modules/wiremod/shell/scanner.dm | 62 ++ code/modules/wiremod/shell/scanner_gate.dm | 66 ++ code/modules/wiremod/shell/server.dm | 24 + code/modules/wiremod/shell/shell_items.dm | 65 ++ config/entries/logging.txt | 3 + tgstation.dme | 1 + 110 files changed, 7353 insertions(+), 259 deletions(-) create mode 100644 code/modules/tgui/status_composers.dm create mode 100644 code/modules/wiremod/components/abstract/compare.dm create mode 100644 code/modules/wiremod/components/abstract/module.dm create mode 100644 code/modules/wiremod/components/action/light.dm create mode 100644 code/modules/wiremod/components/action/mmi.dm create mode 100644 code/modules/wiremod/components/action/pathfind.dm create mode 100644 code/modules/wiremod/components/action/pull.dm create mode 100644 code/modules/wiremod/components/action/radio.dm create mode 100644 code/modules/wiremod/components/action/soundemitter.dm create mode 100644 code/modules/wiremod/components/action/speech.dm create mode 100644 code/modules/wiremod/components/admin/getvar.dm create mode 100644 code/modules/wiremod/components/admin/proccall.dm create mode 100644 code/modules/wiremod/components/admin/sdql.dm create mode 100644 code/modules/wiremod/components/admin/setvar.dm create mode 100644 code/modules/wiremod/components/admin/spawn.dm create mode 100644 code/modules/wiremod/components/admin/to_type.dm create mode 100644 code/modules/wiremod/components/atom/direction.dm create mode 100644 code/modules/wiremod/components/atom/gps.dm create mode 100644 code/modules/wiremod/components/atom/health.dm create mode 100644 code/modules/wiremod/components/atom/hear.dm create mode 100644 code/modules/wiremod/components/atom/self.dm create mode 100644 code/modules/wiremod/components/atom/species.dm create mode 100644 code/modules/wiremod/components/hud/bar_overlay.dm create mode 100644 code/modules/wiremod/components/hud/counter_overlay.dm create mode 100644 code/modules/wiremod/components/hud/object_overlay.dm create mode 100644 code/modules/wiremod/components/hud/target_intercept.dm create mode 100644 code/modules/wiremod/components/list/concat.dm create mode 100644 code/modules/wiremod/components/list/get_column.dm create mode 100644 code/modules/wiremod/components/list/index.dm create mode 100644 code/modules/wiremod/components/list/index_table.dm create mode 100644 code/modules/wiremod/components/list/list_literal.dm create mode 100644 code/modules/wiremod/components/list/select.dm create mode 100644 code/modules/wiremod/components/list/split.dm create mode 100644 code/modules/wiremod/components/math/arithmetic.dm create mode 100644 code/modules/wiremod/components/math/comparison.dm create mode 100644 code/modules/wiremod/components/math/length.dm create mode 100644 code/modules/wiremod/components/math/logic.dm create mode 100644 code/modules/wiremod/components/math/not.dm create mode 100644 code/modules/wiremod/components/math/random.dm create mode 100644 code/modules/wiremod/components/ntnet/ntnet_receive.dm create mode 100644 code/modules/wiremod/components/ntnet/ntnet_send.dm create mode 100644 code/modules/wiremod/components/sensors/pressuresensor.dm create mode 100644 code/modules/wiremod/components/sensors/tempsensor.dm create mode 100644 code/modules/wiremod/components/string/concat.dm create mode 100644 code/modules/wiremod/components/string/contains.dm create mode 100644 code/modules/wiremod/components/string/textcase.dm create mode 100644 code/modules/wiremod/components/string/tonumber.dm create mode 100644 code/modules/wiremod/components/string/tostring.dm create mode 100644 code/modules/wiremod/components/utility/clock.dm create mode 100644 code/modules/wiremod/components/utility/delay.dm create mode 100644 code/modules/wiremod/components/utility/getter.dm create mode 100644 code/modules/wiremod/components/utility/router.dm create mode 100644 code/modules/wiremod/components/utility/setter.dm create mode 100644 code/modules/wiremod/components/utility/typecast.dm create mode 100644 code/modules/wiremod/components/utility/typecheck.dm create mode 100644 code/modules/wiremod/core/admin_panel.dm create mode 100644 code/modules/wiremod/core/component.dm create mode 100644 code/modules/wiremod/core/component_printer.dm create mode 100644 code/modules/wiremod/core/datatypes.dm create mode 100644 code/modules/wiremod/core/duplicator.dm create mode 100644 code/modules/wiremod/core/integrated_circuit.dm create mode 100644 code/modules/wiremod/core/marker.dm create mode 100644 code/modules/wiremod/core/port.dm create mode 100644 code/modules/wiremod/core/usb_cable.dm create mode 100644 code/modules/wiremod/core/variable.dm create mode 100644 code/modules/wiremod/datatypes/any.dm create mode 100644 code/modules/wiremod/datatypes/basic.dm create mode 100644 code/modules/wiremod/datatypes/entity.dm create mode 100644 code/modules/wiremod/datatypes/number.dm create mode 100644 code/modules/wiremod/datatypes/option.dm create mode 100644 code/modules/wiremod/datatypes/signal.dm create mode 100644 code/modules/wiremod/datatypes/string.dm create mode 100644 code/modules/wiremod/preset/hello_world.dm create mode 100644 code/modules/wiremod/preset/speech_relay.dm create mode 100644 code/modules/wiremod/shell/airlock.dm create mode 100644 code/modules/wiremod/shell/bot.dm create mode 100644 code/modules/wiremod/shell/brain_computer_interface.dm create mode 100644 code/modules/wiremod/shell/compact_remote.dm create mode 100644 code/modules/wiremod/shell/controller.dm create mode 100644 code/modules/wiremod/shell/drone.dm create mode 100644 code/modules/wiremod/shell/moneybot.dm create mode 100644 code/modules/wiremod/shell/scanner.dm create mode 100644 code/modules/wiremod/shell/scanner_gate.dm create mode 100644 code/modules/wiremod/shell/server.dm create mode 100644 code/modules/wiremod/shell/shell_items.dm diff --git a/code/__DEFINES/colors.dm b/code/__DEFINES/colors.dm index 04c91cfb9d..3a0af1394a 100644 --- a/code/__DEFINES/colors.dm +++ b/code/__DEFINES/colors.dm @@ -60,3 +60,12 @@ #define COLOR_GRAY "#808080" #define COLOR_HALF_TRANSPARENT_BLACK "#0000007A" #define COLOR_BRIGHT_BLUE "#2CB2E8" + +///Main colors for UI themes +#define COLOR_THEME_MIDNIGHT "#6086A0" +#define COLOR_THEME_PLASMAFIRE "#FFB200" +#define COLOR_THEME_RETRO "#24CA00" +#define COLOR_THEME_SLIMECORE "#4FB259" +#define COLOR_THEME_OPERATIVE "#B8221F" +#define COLOR_THEME_GLASS "#75A4C4" +#define COLOR_THEME_CLOCKWORK "#CFBA47" diff --git a/code/__DEFINES/dcs/signals.dm b/code/__DEFINES/dcs/signals.dm index 33683d20ef..637c7f1b17 100644 --- a/code/__DEFINES/dcs/signals.dm +++ b/code/__DEFINES/dcs/signals.dm @@ -45,6 +45,8 @@ #define COMSIG_PARENT_QDELETING "parent_qdeleting" /// generic topic handler (usr, href_list) #define COMSIG_TOPIC "handle_topic" +/// from datum ui_act (usr, action) +#define COMSIG_UI_ACT "COMSIG_UI_ACT" /// fires on the target datum when an element is attached to it (/datum/element) #define COMSIG_ELEMENT_ATTACH "element_attach" diff --git a/code/__HELPERS/_logging.dm b/code/__HELPERS/_logging.dm index d61adf4337..4bc9ccaacd 100644 --- a/code/__HELPERS/_logging.dm +++ b/code/__HELPERS/_logging.dm @@ -88,6 +88,19 @@ if (CONFIG_GET(flag/log_access)) WRITE_LOG(GLOB.world_game_log, "ACCESS: [text]") +/** + * Writes to a special log file if the log_suspicious_login config flag is set, + * which is intended to contain all logins that failed under suspicious circumstances. + * + * Mirrors this log entry to log_access when access_log_mirror is TRUE, so this proc + * doesn't need to be used alongside log_access and can replace it where appropriate. + */ +/proc/log_suspicious_login(text, access_log_mirror = TRUE) + if (CONFIG_GET(flag/log_suspicious_login)) + WRITE_LOG(GLOB.world_suspicious_login_log, "SUSPICIOUS_ACCESS: [text]") + if(access_log_mirror) + log_access(text) + /proc/log_law(text) if (CONFIG_GET(flag/log_law)) WRITE_LOG(GLOB.world_game_log, "LAW: [text]") diff --git a/code/__HELPERS/unsorted.dm b/code/__HELPERS/unsorted.dm index daad573c5a..e219e1b521 100644 --- a/code/__HELPERS/unsorted.dm +++ b/code/__HELPERS/unsorted.dm @@ -663,10 +663,10 @@ Turf and target are separate in case you want to teleport some distance from a t return locate(final_x, final_y, T.z) //Finds the distance between two atoms, in pixels -//centered = 0 counts from turf edge to edge -//centered = 1 counts from turf center to turf center +//centered = FALSE counts from turf edge to edge +//centered = TRUE counts from turf center to turf center //of course mathematically this is just adding world.icon_size on again -/proc/getPixelDistance(atom/A, atom/B, centered = 1) +/proc/getPixelDistance(atom/A, atom/B, centered = TRUE) if(!istype(A)||!istype(B)) return 0 . = bounds_dist(A, B) + sqrt((((A.pixel_x+B.pixel_x)**2) + ((A.pixel_y+B.pixel_y)**2))) diff --git a/code/_globalvars/logging.dm b/code/_globalvars/logging.dm index 78d802dbbf..843eab6fb0 100644 --- a/code/_globalvars/logging.dm +++ b/code/_globalvars/logging.dm @@ -2,6 +2,9 @@ GLOBAL_VAR(log_directory) GLOBAL_PROTECT(log_directory) GLOBAL_VAR(world_game_log) GLOBAL_PROTECT(world_game_log) +/// Log associated with [/proc/log_suspicious_login()] - Intended to hold all logins that failed due to suspicious circumstances such as ban detection, CID randomisation etc. +GLOBAL_VAR(world_suspicious_login_log) +GLOBAL_PROTECT(world_suspicious_login_log) GLOBAL_VAR(world_runtime_log) GLOBAL_PROTECT(world_runtime_log) GLOBAL_VAR(world_qdel_log) diff --git a/code/_onclick/hud/screen_objects.dm b/code/_onclick/hud/screen_objects.dm index 8590e056c9..496a9611e9 100644 --- a/code/_onclick/hud/screen_objects.dm +++ b/code/_onclick/hud/screen_objects.dm @@ -56,16 +56,12 @@ maptext_width = 480 /atom/movable/screen/swap_hand - layer = HUD_LAYER plane = HUD_PLANE name = "swap hand" /atom/movable/screen/swap_hand/Click() // At this point in client Click() code we have passed the 1/10 sec check and little else // We don't even know if it's a middle click - // if(world.time <= usr.next_move) - // return 1 - if(usr.incapacitated()) return 1 @@ -74,6 +70,17 @@ M.swap_hand() return 1 +// /atom/movable/screen/skills +// name = "skills" +// icon = 'icons/mob/screen_midnight.dmi' +// icon_state = "skills" +// screen_loc = ui_skill_menu + +// /atom/movable/screen/skills/Click() +// if(ishuman(usr)) +// var/mob/living/carbon/human/H = usr +// H.mind.print_levels(H) + /atom/movable/screen/craft name = "crafting menu" icon = 'icons/mob/screen_midnight.dmi' @@ -91,7 +98,7 @@ return TRUE var/area/A = get_area(usr) if(!A.outdoors) - to_chat(usr, "There is already a defined structure here.") + to_chat(usr, span_warning("There is already a defined structure here.")) return TRUE create_area(usr) @@ -114,15 +121,12 @@ /// Icon when contains an item. For now used only by humans. var/icon_full /// The overlay when hovering over with an item in your hand - var/list/object_overlays = list() - layer = HUD_LAYER + var/image/object_overlay plane = HUD_PLANE /atom/movable/screen/inventory/Click(location, control, params) // At this point in client Click() code we have passed the 1/10 sec check and little else // We don't even know if it's a middle click - // if(world.time <= usr.next_move) - // return TRUE if(usr.incapacitated()) // ignore_stasis = TRUE return TRUE @@ -138,42 +142,23 @@ usr.update_inv_hands() return TRUE -/atom/movable/screen/inventory/MouseEntered() - ..() +/atom/movable/screen/inventory/MouseEntered(location, control, params) + . = ..() add_overlays() - //Apply the outline affect - add_stored_outline() /atom/movable/screen/inventory/MouseExited() ..() - cut_overlay(object_overlays) - object_overlays.Cut() - remove_stored_outline() - -/atom/movable/screen/inventory/proc/add_stored_outline() - if(hud?.mymob && slot_id) - var/obj/item/inv_item = hud.mymob.get_item_by_slot(slot_id) - if(inv_item) - if(hud?.mymob.incapacitated() || (slot_id in hud?.mymob.check_obscured_slots()) || !hud?.mymob.canUnEquip(inv_item)) - inv_item.apply_outline(_size = 3) - else - inv_item.apply_outline() - -/atom/movable/screen/inventory/proc/remove_stored_outline() - if(hud?.mymob && slot_id) - var/obj/item/inv_item = hud.mymob.get_item_by_slot(slot_id) - if(inv_item) - inv_item.remove_outline() + cut_overlay(object_overlay) + QDEL_NULL(object_overlay) /atom/movable/screen/inventory/update_icon_state() if(!icon_empty) icon_empty = icon_state - if(hud?.mymob && slot_id && icon_full) - if(hud.mymob.get_item_by_slot(slot_id)) - icon_state = icon_full - else - icon_state = icon_empty + if(!hud?.mymob || !slot_id || !icon_full) + return ..() + icon_state = hud.mymob.get_item_by_slot(slot_id) ? icon_full : icon_empty + return ..() /atom/movable/screen/inventory/proc/add_overlays() var/mob/user = hud?.mymob @@ -189,14 +174,14 @@ var/image/item_overlay = image(holding) item_overlay.alpha = 92 - if(!user.can_equip(holding, slot_id, TRUE, TRUE)) + if(!user.can_equip(holding, slot_id, disable_warning = TRUE, bypass_equip_delay_self = TRUE)) item_overlay.color = "#FF0000" else item_overlay.color = "#00ff00" - cut_overlay(object_overlays) - // object_overlay = item_overlay - add_overlay(object_overlays) + cut_overlay(object_overlay) + object_overlay = item_overlay + add_overlay(object_overlay) /atom/movable/screen/inventory/hand var/mutable_appearance/handcuff_overlay @@ -232,8 +217,6 @@ var/mob/user = hud?.mymob if(usr != user) return TRUE - // if(world.time <= user.next_move) - // return TRUE if(user.incapacitated()) return TRUE if (ismecha(user.loc)) // stops inventory actions in a mech @@ -247,12 +230,24 @@ user.swap_hand(held_index) return TRUE +// /atom/movable/screen/close +// name = "close" +// plane = ABOVE_HUD_PLANE +// icon_state = "backpack_close" + +// /atom/movable/screen/close/Initialize(mapload, new_master) +// . = ..() +// master = new_master + +// /atom/movable/screen/close/Click() +// var/datum/component/storage/S = master +// S.hide_from(usr) +// return TRUE /atom/movable/screen/drop name = "drop" icon = 'icons/mob/screen_midnight.dmi' icon_state = "act_drop" - layer = HUD_LAYER plane = HUD_PLANE /atom/movable/screen/drop/Click() @@ -308,12 +303,12 @@ if(C.internal) C.internal = null - to_chat(C, "You are no longer running on internals.") + to_chat(C, span_notice("You are no longer running on internals.")) icon_state = "internal0" else if(!C.getorganslot(ORGAN_SLOT_BREATHING_TUBE)) if(HAS_TRAIT(C, TRAIT_NO_INTERNALS)) - to_chat(C, "Due to cumbersome equipment or anatomy, you are currently unable to use internals!") + to_chat(C, span_warning("Due to cumbersome equipment or anatomy, you are currently unable to use internals!")) return var/obj/item/clothing/check var/internals = FALSE @@ -326,37 +321,37 @@ if(CHECK_BITFIELD(check.clothing_flags, ALLOWINTERNALS)) internals = TRUE if(!internals) - to_chat(C, "You are not wearing an internals mask!") + to_chat(C, span_warning("You are not wearing an internals mask!")) return var/obj/item/I = C.is_holding_item_of_type(/obj/item/tank) if(I) - to_chat(C, "You are now running on internals from [I] in your [C.get_held_index_name(C.get_held_index_of_item(I))].") + to_chat(C, span_notice("You are now running on internals from [I] in your [C.get_held_index_name(C.get_held_index_of_item(I))].")) C.internal = I else if(ishuman(C)) var/mob/living/carbon/human/H = C if(istype(H.s_store, /obj/item/tank)) - to_chat(H, "You are now running on internals from [H.s_store] on your [H.wear_suit.name].") + to_chat(H, span_notice("You are now running on internals from [H.s_store] on your [H.wear_suit.name].")) H.internal = H.s_store else if(istype(H.belt, /obj/item/tank)) - to_chat(H, "You are now running on internals from [H.belt] on your belt.") + to_chat(H, span_notice("You are now running on internals from [H.belt] on your belt.")) H.internal = H.belt else if(istype(H.l_store, /obj/item/tank)) - to_chat(H, "You are now running on internals from [H.l_store] in your left pocket.") + to_chat(H, span_notice("You are now running on internals from [H.l_store] in your left pocket.")) H.internal = H.l_store else if(istype(H.r_store, /obj/item/tank)) - to_chat(H, "You are now running on internals from [H.r_store] in your right pocket.") + to_chat(H, span_notice("You are now running on internals from [H.r_store] in your right pocket.")) H.internal = H.r_store //Separate so CO2 jetpacks are a little less cumbersome. if(!C.internal && istype(C.back, /obj/item/tank)) - to_chat(C, "You are now running on internals from [C.back] on your back.") + to_chat(C, span_notice("You are now running on internals from [C.back] on your back.")) C.internal = C.back if(C.internal) icon_state = "internal1" else - to_chat(C, "You don't have an oxygen tank!") + to_chat(C, span_warning("You don't have an oxygen tank!")) return C.update_action_buttons_icon() @@ -378,6 +373,7 @@ icon_state = CONFIG_GET(flag/sprint_enabled)? "walking" : "walking_nosprint" if(MOVE_INTENT_RUN) icon_state = CONFIG_GET(flag/sprint_enabled)? "running" : "running_nosprint" + return ..() /atom/movable/screen/mov_intent/proc/toggle(mob/user) if(isobserver(user)) @@ -399,12 +395,12 @@ icon_state = "pull" else icon_state = "pull0" + return ..() /atom/movable/screen/resist name = "resist" icon = 'icons/mob/screen_midnight.dmi' icon_state = "act_resist" - layer = HUD_LAYER plane = HUD_PLANE /atom/movable/screen/resist/Click() @@ -416,7 +412,6 @@ name = "rest" icon = 'icons/mob/screen_midnight.dmi' icon_state = "act_rest" - layer = HUD_LAYER plane = HUD_PLANE /atom/movable/screen/rest/Click() @@ -427,11 +422,12 @@ /atom/movable/screen/rest/update_icon_state() var/mob/living/user = hud?.mymob if(!istype(user)) - return + return ..() if(!user.resting) icon_state = "act_rest" else icon_state = "act_rest0" + return ..() /atom/movable/screen/throw_catch name = "throw/catch" @@ -455,9 +451,9 @@ if(isobserver(usr)) return - var/list/PL = params2list(params) - var/icon_x = text2num(PL["icon-x"]) - var/icon_y = text2num(PL["icon-y"]) + var/list/modifiers = params2list(params) + var/icon_x = text2num(LAZYACCESS(modifiers, "icon-x")) + var/icon_y = text2num(LAZYACCESS(modifiers, "icon-y")) var/choice = get_zone_at(icon_x, icon_y) if (!choice) return 1 @@ -465,15 +461,16 @@ return set_selected_zone(choice, usr) /atom/movable/screen/zone_sel/MouseEntered(location, control, params) + . = ..() MouseMove(location, control, params) /atom/movable/screen/zone_sel/MouseMove(location, control, params) if(isobserver(usr)) return - var/list/PL = params2list(params) - var/icon_x = text2num(PL["icon-x"]) - var/icon_y = text2num(PL["icon-y"]) + var/list/modifiers = params2list(params) + var/icon_x = text2num(LAZYACCESS(modifiers, "icon-x")) + var/icon_y = text2num(LAZYACCESS(modifiers, "icon-y")) var/choice = get_zone_at(icon_x, icon_y) if(hovering == choice) @@ -493,7 +490,6 @@ mouse_opacity = MOUSE_OPACITY_TRANSPARENT alpha = 128 anchored = TRUE - layer = ABOVE_HUD_LAYER plane = ABOVE_HUD_PLANE /atom/movable/screen/zone_sel/MouseExited(location, control, params) @@ -545,7 +541,7 @@ if(choice != hud.mymob.zone_selected) hud.mymob.zone_selected = choice - update_icon() + update_appearance() return TRUE @@ -562,7 +558,6 @@ /atom/movable/screen/zone_sel/robot icon = 'icons/mob/screen_cyborg.dmi' - /atom/movable/screen/flash name = "flash" icon_state = "blank" @@ -651,6 +646,11 @@ name = "health doll" screen_loc = ui_healthdoll +/atom/movable/screen/healthdoll/Click() + if (iscarbon(usr)) + var/mob/living/carbon/C = usr + C.check_self_for_injuries() + /atom/movable/screen/healthdoll/living icon_state = "fullhealth0" screen_loc = ui_living_healthdoll @@ -661,6 +661,9 @@ icon_state = "mood5" screen_loc = ui_mood +/atom/movable/screen/mood/attack_tk() + return + /atom/movable/screen/splash icon = 'icons/blank_title.png' icon_state = "" @@ -671,6 +674,8 @@ /atom/movable/screen/splash/New(client/C, visible, use_previous_title) //TODO: Make this use INITIALIZE_IMMEDIATE, except its not easy . = ..() + if(!istype(C)) + return holder = C diff --git a/code/controllers/configuration/entries/logging.dm b/code/controllers/configuration/entries/logging.dm index 25ca145939..54bd5cb713 100644 --- a/code/controllers/configuration/entries/logging.dm +++ b/code/controllers/configuration/entries/logging.dm @@ -4,6 +4,10 @@ /datum/config_entry/flag/log_access // log login/logout config_entry_value = TRUE +/// Config entry which special logging of failed logins under suspicious circumstances. +/datum/config_entry/flag/log_suspicious_login + config_entry_value = TRUE + /datum/config_entry/flag/log_say // log client say config_entry_value = TRUE diff --git a/code/datums/browser.dm b/code/datums/browser.dm index 1663f8b5c6..bce948e8d9 100644 --- a/code/datums/browser.dm +++ b/code/datums/browser.dm @@ -4,7 +4,7 @@ var/window_id // window_id is used as the window name for browse and onclose var/width = 0 var/height = 0 - var/atom/ref = null + var/datum/weakref/ref = null var/window_options = "can_close=1;can_minimize=1;can_maximize=0;can_resize=1;titlebar=1;" // window option is set using window_id var/stylesheets[0] var/scripts[0] @@ -16,8 +16,8 @@ /datum/browser/New(nuser, nwindow_id, ntitle = 0, nwidth = 0, nheight = 0, atom/nref = null) - user = nuser + RegisterSignal(user, COMSIG_PARENT_QDELETING, .proc/user_deleted) window_id = nwindow_id if (ntitle) title = format_text(ntitle) @@ -26,7 +26,11 @@ if (nheight) height = nheight if (nref) - ref = nref + ref = WEAKREF(nref) + +/datum/browser/proc/user_deleted(datum/source) + SIGNAL_HANDLER + user = null /datum/browser/proc/add_head_content(nhead_content) head_content = nhead_content @@ -35,7 +39,7 @@ window_options = nwindow_options /datum/browser/proc/add_stylesheet(name, file) - if(istype(name, /datum/asset/spritesheet)) + if (istype(name, /datum/asset/spritesheet)) var/datum/asset/spritesheet/sheet = name stylesheets["spritesheet_[sheet.name].css"] = "data/spritesheets/[sheet.name]" else @@ -94,27 +98,32 @@ "} /datum/browser/proc/open(use_onclose = TRUE) - if(isnull(window_id)) //null check because this can potentially nuke goonchat + if(isnull(window_id)) //null check because this can potentially nuke goonchat WARNING("Browser [title] tried to open with a null ID") - to_chat(user, "The [title] browser you tried to open failed a sanity check! Please report this on github!") + to_chat(user, span_userdanger("The [title] browser you tried to open failed a sanity check! Please report this on github!")) return var/window_size = "" - if(width && height) + if (width && height) window_size = "size=[width]x[height];" common_asset.send(user) - if(stylesheets.len) + if (stylesheets.len) SSassets.transport.send_assets(user, stylesheets) - if(scripts.len) + if (scripts.len) SSassets.transport.send_assets(user, scripts) user << browse(get_content(), "window=[window_id];[window_size][window_options]") - if(use_onclose) + if (use_onclose) setup_onclose() /datum/browser/proc/setup_onclose() set waitfor = 0 //winexists sleeps, so we don't need to. for (var/i in 1 to 10) - if (user && winexists(user, window_id)) - onclose(user, window_id, ref) + if (user?.client && winexists(user, window_id)) + var/atom/send_ref + if(ref) + send_ref = ref.resolve() + if(!send_ref) + ref = null + onclose(user, window_id, send_ref) break /datum/browser/proc/close() @@ -153,11 +162,35 @@ opentime = 0 close() -//designed as a drop in replacement for alert(); functions the same. (outside of needing User specified) -/proc/tgalert(mob/User, Message, Title, Button1="Ok", Button2, Button3, StealFocus = 1, Timeout = 6000) +/** + * **DEPRECATED: USE tgui_alert(...) INSTEAD** + * + * Designed as a drop in replacement for alert(); functions the same. (outside of needing User specified) + * Arguments: + * * User - The user to show the alert to. + * * Message - The textual body of the alert. + * * Title - The title of the alert's window. + * * Button1 - The first button option. + * * Button2 - The second button option. + * * Button3 - The third button option. + * * StealFocus - Boolean operator controlling if the alert will steal the user's window focus. + * * Timeout - The timeout of the window, after which no responses will be valid. + */ +/proc/tgalert(mob/User, Message, Title, Button1="Ok", Button2, Button3, StealFocus = TRUE, Timeout = 6000) if (!User) User = usr - switch(askuser(User, Message, Title, Button1, Button2, Button3, StealFocus, Timeout)) + if (!istype(User)) + if (istype(User, /client)) + var/client/client = User + User = client.mob + else + return + + // Get user's response using a modal + var/datum/browser/modal/alert/A = new(User, Message, Title, Button1, Button2, Button3, StealFocus, Timeout) + A.open() + A.wait() + switch(A.selectedbutton) if (1) return Button1 if (2) @@ -197,8 +230,8 @@ .=..() opentime = 0 -/datum/browser/modal/open(use_onclose = 1) - set waitfor = 0 +/datum/browser/modal/open(use_onclose) + set waitfor = FALSE opentime = world.time if (stealfocus) @@ -277,7 +310,7 @@ opentime = 0 close() -/proc/presentpicker(var/mob/User,Message, Title, Button1="Ok", Button2, Button3, StealFocus = 1,Timeout = 6000,list/values, inputtype = "checkbox", width, height, slidecolor) +/proc/presentpicker(mob/User,Message, Title, Button1="Ok", Button2, Button3, StealFocus = 1,Timeout = 6000,list/values, inputtype = "checkbox", width, height, slidecolor) if (!istype(User)) if (istype(User, /client/)) var/client/C = User @@ -290,7 +323,7 @@ if (A.selectedbutton) return list("button" = A.selectedbutton, "values" = A.valueslist) -/proc/input_bitfield(var/mob/User, title, bitfield, current_value, nwidth = 350, nheight = 350, nslidecolor, allowed_edit_list = null) +/proc/input_bitfield(mob/User, title, bitfield, current_value, nwidth = 350, nheight = 350, nslidecolor, allowed_edit_list = null) if (!User || !(bitfield in GLOB.bitfields)) return var/list/pickerlist = list() @@ -376,13 +409,13 @@ if ("string") settings["mainsettings"][setting]["value"] = stripped_input(user, "Enter new value for [settings["mainsettings"][setting]["desc"]]", "Enter new value for [settings["mainsettings"][setting]["desc"]]", settings["mainsettings"][setting]["value"]) if ("number") - settings["mainsettings"][setting]["value"] = tgui_input_num(user, "Enter new value for [settings["mainsettings"][setting]["desc"]]", "Enter new value for [settings["mainsettings"][setting]["desc"]]") + settings["mainsettings"][setting]["value"] = input(user, "Enter new value for [settings["mainsettings"][setting]["desc"]]", "Enter new value for [settings["mainsettings"][setting]["desc"]]") as num if ("color") settings["mainsettings"][setting]["value"] = input(user, "Enter new value for [settings["mainsettings"][setting]["desc"]]", "Enter new value for [settings["mainsettings"][setting]["desc"]]", settings["mainsettings"][setting]["value"]) as color if ("boolean") - settings["mainsettings"][setting]["value"] = tgui_input_list(user, "[settings["mainsettings"][setting]["desc"]]?", "", list("Yes","No")) + settings["mainsettings"][setting]["value"] = (settings["mainsettings"][setting]["value"] == "Yes") ? "No" : "Yes" if ("ckey") - settings["mainsettings"][setting]["value"] = tgui_input_list(user, "[settings["mainsettings"][setting]["desc"]]?", "", list("none") + GLOB.directory) + settings["mainsettings"][setting]["value"] = input(user, "[settings["mainsettings"][setting]["desc"]]?") in list("none") + GLOB.directory if (settings["mainsettings"][setting]["callback"]) var/datum/callback/callback = settings["mainsettings"][setting]["callback"] settings = callback.Invoke(settings) @@ -401,7 +434,7 @@ opentime = 0 close() -/proc/presentpreflikepicker(var/mob/User,Message, Title, Button1="Ok", Button2, Button3, StealFocus = 1,Timeout = 6000,list/settings, width, height, slidecolor) +/proc/presentpreflikepicker(mob/User,Message, Title, Button1="Ok", Button2, Button3, StealFocus = 1,Timeout = 6000,list/settings, width, height, slidecolor) if (!istype(User)) if (istype(User, /client/)) var/client/C = User @@ -414,12 +447,6 @@ if (A.selectedbutton) return list("button" = A.selectedbutton, "settings" = A.settings) -// This will allow you to show an icon in the browse window -// This is added to mob so that it can be used without a reference to the browser object -// There is probably a better place for this... -/mob/proc/browse_rsc_icon(icon, icon_state, dir = -1) - - // Registers the on-close verb for a browse window (client/verb/.windowclose) // this will be called when the close-button of a window is pressed. // @@ -427,8 +454,8 @@ // e.g. canisters, timers, etc. // // windowid should be the specified window name -// e.g. code is : user << browse(text, "window=fred") -// then use : onclose(user, "fred") +// e.g. code is : user << browse(text, "window=fred") +// then use : onclose(user, "fred") // // Optionally, specify the "ref" parameter as the controlled atom (usually src) // to pass a "close=1" parameter to the atom's Topic() proc for special handling. @@ -451,18 +478,18 @@ // otherwise, just reset the client mob's machine var. // /client/verb/windowclose(atomref as text) - set hidden = TRUE // hide this verb from the user's panel - set name = ".windowclose" // no autocomplete on cmd line + set hidden = TRUE // hide this verb from the user's panel + set name = ".windowclose" // no autocomplete on cmd line - if(atomref!="null") // if passed a real atomref - var/hsrc = locate(atomref) // find the reffed atom + if(atomref!="null") // if passed a real atomref + var/hsrc = locate(atomref) // find the reffed atom var/href = "close=1" if(hsrc) usr = src.mob - src.Topic(href, params2list(href), hsrc) // this will direct to the atom's - return // Topic() proc via client.Topic() + src.Topic(href, params2list(href), hsrc) // this will direct to the atom's + return // Topic() proc via client.Topic() // no atomref specified (or not found) // so just reset the user mob's machine var - if(src && src.mob) + if(src?.mob) src.mob.unset_machine() diff --git a/code/datums/components/storage/concrete/_concrete.dm b/code/datums/components/storage/concrete/_concrete.dm index bb226bbfdf..6f08959dd2 100644 --- a/code/datums/components/storage/concrete/_concrete.dm +++ b/code/datums/components/storage/concrete/_concrete.dm @@ -66,7 +66,7 @@ /datum/component/storage/concrete/_insert_physical_item(obj/item/I, override = FALSE) . = TRUE var/atom/real_location = real_location() - if(I.loc != real_location && real_location) + if(real_location && I.loc != real_location) I.forceMove(real_location) refresh_mob_views() @@ -95,16 +95,19 @@ return FALSE /datum/component/storage/concrete/proc/on_contents_del(datum/source, atom/A) + SIGNAL_HANDLER var/atom/real_location = parent if(A in real_location) usr = null remove_from_storage(A, null) /datum/component/storage/concrete/proc/on_deconstruct(datum/source, disassembled) + SIGNAL_HANDLER if(drop_all_on_deconstruct) do_quick_empty() /datum/component/storage/concrete/proc/on_break(datum/source, damage_flag) + SIGNAL_HANDLER if(drop_all_on_break) do_quick_empty() if(unlock_on_break) @@ -131,14 +134,16 @@ var/list/seeing_mobs = can_see_contents() for(var/mob/M in seeing_mobs) M.client.screen -= AM - if(ismob(parent.loc) && isitem(AM)) - var/obj/item/I = AM - var/mob/M = parent.loc - I.dropped(M) - I.item_flags &= ~IN_STORAGE - I.remove_outline() + if(isitem(AM)) + var/obj/item/removed_item = AM + removed_item.item_flags &= ~IN_STORAGE + if(ismob(parent.loc)) + var/mob/carrying_mob = parent.loc + removed_item.dropped(carrying_mob, TRUE) if(new_location) - AM.forceMove(new_location) // exited comsig will handle removal reset. + //Reset the items values + _removal_reset(AM) + AM.forceMove(new_location) //We don't want to call this if the item is being destroyed AM.on_exit_storage(src) else @@ -147,7 +152,7 @@ refresh_mob_views() if(isobj(parent)) var/obj/O = parent - O.update_icon() + O.update_appearance() return TRUE /datum/component/storage/concrete/proc/slave_can_insert_object(datum/component/storage/slave, obj/item/I, stop_messages = FALSE, mob/M) @@ -158,7 +163,7 @@ if(. && !prevent_warning) slave.mob_item_insertion_feedback(usr, M, I) -/datum/component/storage/concrete/handle_item_insertion(obj/item/I, prevent_warning = FALSE, mob/M, datum/component/storage/remote) //Remote is null or the slave datum +/datum/component/storage/concrete/handle_item_insertion(obj/item/I, prevent_warning = FALSE, mob/M, datum/component/storage/remote) //Remote is null or the slave datum var/datum/component/storage/concrete/master = master() var/atom/parent = src.parent var/moved = FALSE @@ -168,7 +173,7 @@ if(!M.temporarilyRemoveItemFromInventory(I)) return FALSE else - moved = TRUE //At this point if the proc fails we need to manually move the object back to the turf/mob/whatever. + moved = TRUE //At this point if the proc fails we need to manually move the object back to the turf/mob/whatever. if(I.pulledby) I.pulledby.stop_pulling() if(silent) @@ -203,7 +208,7 @@ /datum/component/storage/concrete/update_icon() if(isobj(parent)) var/obj/O = parent - O.update_icon() + O.update_appearance() for(var/i in slaves) var/datum/component/storage/slave = i slave.update_icon() diff --git a/code/datums/components/storage/storage.dm b/code/datums/components/storage/storage.dm index 97d6748dc7..ce1489418f 100644 --- a/code/datums/components/storage/storage.dm +++ b/code/datums/components/storage/storage.dm @@ -418,38 +418,38 @@ return TRUE /datum/component/storage/proc/mousedrop_onto(datum/source, atom/over_object, mob/M) + SIGNAL_HANDLER + set waitfor = FALSE . = COMPONENT_NO_MOUSEDROP + if(!ismob(M)) + return + if(!over_object) + return + if(ismecha(M.loc)) // stops inventory actions in a mech + return + if(M.incapacitated() || !M.canUseStorage()) + return var/atom/A = parent - if(ismob(M)) //all the check for item manipulation are in other places, you can safely open any storages as anything and its not buggy, i checked - A.add_fingerprint(M) - if(istype(A, /obj/item)) - var/obj/item/I = A - I.remove_outline() //Removes the outline when we drag - if(!over_object) - return FALSE - if(ismecha(M.loc)) // stops inventory actions in a mech - return FALSE - // this must come before the screen objects only block, dunno why it wasn't before - if(over_object == M) - user_show_to_mob(M, trigger_on_found = TRUE) - return - if(isrevenant(M)) - RevenantThrow(over_object, M, source) - return - if(!M.incapacitated()) - if(!istype(over_object, /atom/movable/screen)) - dump_content_at(over_object, M) - return - if(A.loc != M) - return - playsound(A, "rustle", 50, 1, -5) - A.do_jiggle() - if(istype(over_object, /atom/movable/screen/inventory/hand)) - var/atom/movable/screen/inventory/hand/H = over_object - M.putItemFromInventoryInHandIfPossible(A, H.held_index) - return - A.add_fingerprint(M) + A.add_fingerprint(M) + // this must come before the screen objects only block, dunno why it wasn't before + if(over_object == M) + user_show_to_mob(M, trigger_on_found = TRUE) + if(isrevenant(M)) + INVOKE_ASYNC(GLOBAL_PROC, .proc/RevenantThrow, over_object, M, source) + return + if(!istype(over_object, /atom/movable/screen)) + INVOKE_ASYNC(src, .proc/dump_content_at, over_object, M) + return + if(A.loc != M) + return + playsound(A, "rustle", 50, TRUE, -5) + A.do_jiggle() + if(istype(over_object, /atom/movable/screen/inventory/hand)) + var/atom/movable/screen/inventory/hand/H = over_object + M.putItemFromInventoryInHandIfPossible(A, H.held_index) + return + A.add_fingerprint(M) /datum/component/storage/proc/user_show_to_mob(mob/M, force = FALSE, trigger_on_found = FALSE) var/atom/A = parent diff --git a/code/datums/materials/_material.dm b/code/datums/materials/_material.dm index 658f22a107..dbd976b2f7 100644 --- a/code/datums/materials/_material.dm +++ b/code/datums/materials/_material.dm @@ -120,7 +120,7 @@ Simple datum which is instanced once per type and is used for every object of sa return I.hitsound = item_sound_override I.usesound = item_sound_override - I.throwhitsound = item_sound_override + I.mob_throw_hit_sound = item_sound_override // I.mob_throw_hit_sound = item_sound_override // I.equip_sound = item_sound_override // I.pickup_sound = item_sound_override diff --git a/code/game/atoms.dm b/code/game/atoms.dm index 5e3db3d93c..81194fd781 100644 --- a/code/game/atoms.dm +++ b/code/game/atoms.dm @@ -1400,3 +1400,15 @@ */ /atom/proc/setClosed() return + +//Update the screentip to reflect what we're hoverin over +/atom/MouseEntered(location, control, params) + . = ..() + // Statusbar + // status_bar_set_text(usr, name) + // Screentips + // if(usr?.hud_used) + // if(!usr.client?.prefs.screentip_pref || (flags_1 & NO_SCREENTIPS_1)) + // usr.hud_used.screentip_text.maptext = "" + // else + // usr.hud_used.screentip_text.maptext = MAPTEXT("[name]") diff --git a/code/game/objects/items.dm b/code/game/objects/items.dm index 53f64c57d2..34f32edb47 100644 --- a/code/game/objects/items.dm +++ b/code/game/objects/items.dm @@ -46,11 +46,23 @@ GLOBAL_VAR_INIT(embedpocalypse, FALSE) // if true, all items will be able to emb max_integrity = 200 obj_flags = NONE + ///Item flags for the item var/item_flags = NONE - var/hitsound = null - var/usesound = null - var/throwhitsound = null + ///Sound played when you hit something with the item + var/hitsound + ///Played when the item is used, for example tools + var/usesound + ///Used when yate into a mob + var/mob_throw_hit_sound + ///Sound used when equipping the item into a valid slot + var/equip_sound + ///Sound uses when picking the item up (into your hands) + var/pickup_sound + ///Sound uses when dropping the item, or when its thrown. + var/drop_sound + ///Whether or not we use stealthy audio levels for this item's attack sounds + var/stealthy_audio = FALSE /// Weight class for how much storage capacity it uses and how big it physically is meaning storages can't hold it if their maximum weight class isn't as high as it. var/w_class = WEIGHT_CLASS_NORMAL @@ -129,7 +141,8 @@ GLOBAL_VAR_INIT(embedpocalypse, FALSE) // if true, all items will be able to emb var/datum/dog_fashion/dog_fashion = null //Tooltip vars - var/force_string //string form of an item's force. Edit this var only to set a custom force string + ///string form of an item's force. Edit this var only to set a custom force string + var/force_string var/last_force_string_check = 0 var/trigger_guard = TRIGGER_GUARD_NONE @@ -340,6 +353,8 @@ GLOBAL_VAR_INIT(embedpocalypse, FALSE) // if true, all items will be able to emb to_chat(user, "You are unable to unequip that while wearing other garments over it!") return FALSE + . = TRUE + if(resistance_flags & ON_FIRE) var/mob/living/carbon/C = user var/can_handle_hot = FALSE @@ -374,6 +389,8 @@ GLOBAL_VAR_INIT(embedpocalypse, FALSE) // if true, all items will be able to emb //If the item is in a storage item, take it out SEND_SIGNAL(loc, COMSIG_TRY_STORAGE_TAKE, src, user.loc, TRUE) + if(QDELETED(src)) //moving it out of the storage to the floor destroyed it. + return if(throwing) throwing.finalize(FALSE) @@ -381,11 +398,12 @@ GLOBAL_VAR_INIT(embedpocalypse, FALSE) // if true, all items will be able to emb if(!allow_attack_hand_drop(user) || !user.temporarilyRemoveItemFromInventory(src)) return - remove_outline() + . = FALSE pickup(user) add_fingerprint(user) if(!user.put_in_active_hand(src, FALSE, FALSE)) user.dropItemToGround(src) + return TRUE /obj/item/proc/allow_attack_hand_drop(mob/user) return TRUE @@ -453,9 +471,8 @@ GLOBAL_VAR_INIT(embedpocalypse, FALSE) // if true, all items will be able to emb DISABLE_BITFIELD(item_flags, IN_INVENTORY) DISABLE_BITFIELD(item_flags, IN_STORAGE) SEND_SIGNAL(src, COMSIG_ITEM_DROPPED,user) - remove_outline() - // if(!silent) - // playsound(src, drop_sound, DROP_SOUND_VOLUME, ignore_walls = FALSE) + if(!silent) + playsound(src, drop_sound, DROP_SOUND_VOLUME, ignore_walls = FALSE) user?.update_equipment_speed_mods() // called just as an item is picked up (loc is not yet changed) @@ -702,8 +719,8 @@ GLOBAL_VAR_INIT(embedpocalypse, FALSE) // if true, all items will be able to emb if(isliving(hit_atom)) //Living mobs handle hit sounds differently. var/volume = get_volume_by_throwforce_and_or_w_class() if (throwforce > 0) - if (throwhitsound) - playsound(hit_atom, throwhitsound, volume, TRUE, -1) + if (mob_throw_hit_sound) + playsound(hit_atom, mob_throw_hit_sound, volume, TRUE, -1) else if(hitsound) playsound(hit_atom, hitsound, volume, TRUE, -1) else @@ -711,8 +728,8 @@ GLOBAL_VAR_INIT(embedpocalypse, FALSE) // if true, all items will be able to emb else playsound(hit_atom, 'sound/weapons/throwtap.ogg', 1, volume, -1) - // else - // playsound(src, drop_sound, YEET_SOUND_VOLUME, ignore_walls = FALSE) + else if (drop_sound) + playsound(src, drop_sound, YEET_SOUND_VOLUME, ignore_walls = FALSE) return hit_atom.hitby(src, 0, itempush, throwingdatum=throwingdatum) /obj/item/throw_at(atom/target, range, speed, mob/thrower, spin=1, diagonals_first = 0, datum/callback/callback, force, messy_throw = TRUE) @@ -886,47 +903,55 @@ GLOBAL_VAR_INIT(embedpocalypse, FALSE) // if true, all items will be able to emb openToolTip(user,src,params,title = name,content = "[desc]
Force: [force_string]",theme = "") /obj/item/MouseEntered(location, control, params) + . = ..() SEND_SIGNAL(src, COMSIG_ITEM_MOUSE_ENTER, location, control, params) - if((item_flags & IN_INVENTORY || item_flags & IN_STORAGE) && usr?.client.prefs.enable_tips && !QDELETED(src)) - var/timedelay = max(usr.client.prefs.tip_delay * 0.01, 0.01) // I heard multiplying is faster, also runtimes from very low/negative numbers - usr.client.tip_timer = addtimer(CALLBACK(src, .proc/openTip, location, control, params, usr), timedelay, TIMER_STOPPABLE)//timer takes delay in deciseconds, but the pref is in milliseconds. dividing by 100 converts it. - var/mob/living/L = usr - if(istype(L) && (L.incapacitated() || (current_equipped_slot in L.check_obscured_slots()) || !L.canUnEquip(src))) - apply_outline(_size = 3) - else - apply_outline() + if(get(src, /mob) == usr && !QDELETED(src)) + var/mob/living/L = usr + if(usr.client.prefs.enable_tips) + var/timedelay = usr.client.prefs.tip_delay/100 + usr.client.tip_timer = addtimer(CALLBACK(src, .proc/openTip, location, control, params, usr), timedelay, TIMER_STOPPABLE)//timer takes delay in deciseconds, but the pref is in milliseconds. dividing by 100 converts it. + // if(usr.client.prefs.itemoutline_pref) + if(istype(L) && L.incapacitated()) + apply_outline(COLOR_RED_GRAY) //if they're dead or handcuffed, let's show the outline as red to indicate that they can't interact with that right now + else + apply_outline() //if the player's alive and well we send the command with no color set, so it uses the theme's color /obj/item/MouseDrop(atom/over, src_location, over_location, src_control, over_control, params) . = ..() - remove_outline() + remove_filter("hover_outline") //get rid of the hover effect in case the mouse exit isn't called if someone drags and drops an item and somthing goes wrong -/obj/item/MouseExited(location,control,params) +/obj/item/MouseExited(location, control, params) SEND_SIGNAL(src, COMSIG_ITEM_MOUSE_EXIT, location, control, params) + deltimer(usr.client.tip_timer) //delete any in-progress timer if the mouse is moved off the item before it finishes closeToolTip(usr) - remove_outline() + remove_filter("hover_outline") -/obj/item/proc/apply_outline(colour = null, _size=1) - if(!(item_flags & IN_INVENTORY || item_flags & IN_STORAGE) || QDELETED(src) || isobserver(usr)) +/obj/item/proc/apply_outline(outline_color = null) + if(get(src, /mob) != usr || QDELETED(src) || isobserver(usr)) //cancel if the item isn't in an inventory, is being deleted, or if the person hovering is a ghost (so that people spectating you don't randomly make your items glow) return - if(usr.client) - if(!usr.client.prefs.outline_enabled) - return - if(!colour) - if(usr.client) - colour = usr.client.prefs.outline_color - if(!colour) - colour = COLOR_BLUE_GRAY - else - colour = COLOR_BLUE_GRAY - if(outline_filter) - filters -= outline_filter - outline_filter = filter(type="outline", size=_size, color=colour) - filters += outline_filter + var/theme = lowertext(usr.client.prefs.UI_style) + if(!outline_color) //if we weren't provided with a color, take the theme's color + switch(theme) //yeah it kinda has to be this way + if("midnight") + outline_color = COLOR_THEME_MIDNIGHT + if("plasmafire") + outline_color = COLOR_THEME_PLASMAFIRE + if("retro") + outline_color = COLOR_THEME_RETRO //just as garish as the rest of this theme + if("slimecore") + outline_color = COLOR_THEME_SLIMECORE + if("operative") + outline_color = COLOR_THEME_OPERATIVE + if("clockwork") + outline_color = COLOR_THEME_CLOCKWORK //if you want free gbp go fix the fact that clockwork's tooltip css is glass' + if("glass") + outline_color = COLOR_THEME_GLASS + else //this should never happen, hopefully + outline_color = COLOR_WHITE + if(color) + outline_color = COLOR_WHITE //if the item is recolored then the outline will be too, let's make the outline white so it becomes the same color instead of some ugly mix of the theme and the tint -/obj/item/proc/remove_outline() - if(outline_filter) - filters -= outline_filter - outline_filter = null + add_filter("hover_outline", 1, list("type" = "outline", "size" = 1, "color" = outline_color)) // Called when a mob tries to use the item as a tool. // Handles most checks. diff --git a/code/modules/client/client_defines.dm b/code/modules/client/client_defines.dm index 950b2b5b25..70aab57e0a 100644 --- a/code/modules/client/client_defines.dm +++ b/code/modules/client/client_defines.dm @@ -34,6 +34,9 @@ var/last_move = 0 var/area = null + /// Timers are now handled by clients, not by doing a mess on the item and multiple people overwriting a single timer on the object, have fun. + var/tip_timer = null + /// Last time we Click()ed. No clicking twice in one tick! var/last_click = 0 diff --git a/code/modules/surgery/tools.dm b/code/modules/surgery/tools.dm index 5c7b71b779..1d2286214d 100644 --- a/code/modules/surgery/tools.dm +++ b/code/modules/surgery/tools.dm @@ -295,7 +295,7 @@ lefthand_file = 'icons/mob/inhands/equipment/medical_lefthand.dmi' righthand_file = 'icons/mob/inhands/equipment/medical_righthand.dmi' hitsound = 'sound/weapons/circsawhit.ogg' - throwhitsound = 'sound/weapons/pierce.ogg' + mob_throw_hit_sound = 'sound/weapons/pierce.ogg' item_flags = SURGICAL_TOOL flags_1 = CONDUCT_1 force = 15 @@ -327,7 +327,7 @@ icon = 'icons/obj/surgery.dmi' icon_state = "saw" hitsound = 'sound/weapons/circsawhit.ogg' - throwhitsound = 'sound/weapons/pierce.ogg' + mob_throw_hit_sound = 'sound/weapons/pierce.ogg' flags_1 = CONDUCT_1 force = 10 w_class = WEIGHT_CLASS_SMALL diff --git a/code/modules/tgchat/to_chat.dm b/code/modules/tgchat/to_chat.dm index 3030ec7fe9..c0e27a2ba3 100644 --- a/code/modules/tgchat/to_chat.dm +++ b/code/modules/tgchat/to_chat.dm @@ -7,18 +7,28 @@ * Circumvents the message queue and sends the message * to the recipient (target) as soon as possible. */ -/proc/to_chat_immediate(target, html, - type = null, - text = null, - avoid_highlighting = FALSE, - // FIXME: These flags are now pointless and have no effect - handle_whitespace = TRUE, - trailing_newline = TRUE, - confidential = FALSE) - if(!target || (!html && !text)) +/proc/to_chat_immediate( + target, + html, + type = null, + text = null, + avoid_highlighting = FALSE, + // FIXME: These flags are now pointless and have no effect + handle_whitespace = TRUE, + trailing_newline = TRUE, + confidential = FALSE +) + // Useful where the integer 0 is the entire message. Use case is enabling to_chat(target, some_boolean) while preventing to_chat(target, "") + html = "[html]" + text = "[text]" + + if(!target) return + if(!html && !text) + CRASH("Empty or null string in to_chat proc call.") if(target == world) target = GLOB.clients + // Build a message var/message = list() if(type) message["type"] = type @@ -53,21 +63,32 @@ * html = "You have found [object]") * ``` */ -/proc/to_chat(target, html, - type = null, - text = null, - avoid_highlighting = FALSE, - // FIXME: These flags are now pointless and have no effect - handle_whitespace = TRUE, - trailing_newline = TRUE, - confidential = FALSE) - if(Master.current_runlevel == RUNLEVEL_INIT || !SSchat?.initialized) - to_chat_immediate(target, html, type, text) +/proc/to_chat( + target, + html, + type = null, + text = null, + avoid_highlighting = FALSE, + // FIXME: These flags are now pointless and have no effect + handle_whitespace = TRUE, + trailing_newline = TRUE, + confidential = FALSE +) + if(isnull(Master) || Master.current_runlevel == RUNLEVEL_INIT || !SSchat?.initialized) + to_chat_immediate(target, html, type, text, avoid_highlighting) return - if(!target || (!html && !text)) + + // Useful where the integer 0 is the entire message. Use case is enabling to_chat(target, some_boolean) while preventing to_chat(target, "") + html = "[html]" + text = "[text]" + + if(!target) return + if(!html && !text) + CRASH("Empty or null string in to_chat proc call.") if(target == world) target = GLOB.clients + // Build a message var/message = list() if(type) message["type"] = type diff --git a/code/modules/tgui/external.dm b/code/modules/tgui/external.dm index d858c2d6bf..69888bd641 100644 --- a/code/modules/tgui/external.dm +++ b/code/modules/tgui/external.dm @@ -75,6 +75,7 @@ */ /datum/proc/ui_act(action, list/params, datum/tgui/ui, datum/ui_state/state) SHOULD_CALL_PARENT(TRUE) + SEND_SIGNAL(src, COMSIG_UI_ACT, usr, action) // If UI is not interactive or usr calling Topic is not the UI user, bail. if(!ui || ui.status != UI_INTERACTIVE) return TRUE diff --git a/code/modules/tgui/status_composers.dm b/code/modules/tgui/status_composers.dm new file mode 100644 index 0000000000..5a8ef843d7 --- /dev/null +++ b/code/modules/tgui/status_composers.dm @@ -0,0 +1,111 @@ +/// The sane defaults for a UI such as a computer or a machine. +/proc/default_ui_state(mob/user, atom/source) + return min( + ui_status_user_is_abled(user, source), + ui_status_user_has_free_hands(user, source), + ui_status_user_is_advanced_tool_user(user), + ui_status_only_living(user), + max( + ui_status_user_is_adjacent(user, source), + ui_status_silicon_has_access(user, source), + ) + ) + +/// Returns a UI status such that users adjacent to source will be able to interact, +/// far away users will be able to see, and anyone farther won't see anything. +/// Dead users will receive updates no matter what, though you likely want to add +/// a [`ui_status_only_living`] check for finer observer interactions. +/proc/ui_status_user_is_adjacent(mob/user, atom/source) + if (isliving(user)) + var/mob/living/living_user = user + return living_user.shared_living_ui_distance(source) + else + return UI_UPDATE + +/// Returns a UI status such that the dead will be able to watch, but not interact. +/proc/ui_status_only_living(mob/user, source) + if (isliving(user)) + return UI_INTERACTIVE + + if(isobserver(user)) + // If they turn on ghost AI control, admins can always interact. + if(IsAdminGhost(user)) + return UI_INTERACTIVE + + // Regular ghosts can always at least view if in range. + var/datum/client_interface/client = GET_CLIENT(user) + if(client) + var/clientviewlist = getviewsize(client.view) + if(get_dist(source, user) < max(clientviewlist[1], clientviewlist[2])) + return UI_UPDATE + + return UI_CLOSE + +/// Returns a UI status such that users with debilitating conditions, such as +/// being dead or not having power for silicons, will not be able to interact. +/// Being dead will disable UI, being incapacitated will continue updating it, +/// and anything else will make it interactive. +/proc/ui_status_user_is_abled(mob/user, atom/source) + return user.shared_ui_interaction(source) + +/// Returns a UI status such that those without blocked hands will be able to interact, +/// but everyone else can only watch. +/proc/ui_status_user_has_free_hands(mob/user, atom/source) + return (!user.restrained(TRUE) && user.get_num_arms() && user.get_empty_held_indexes()) ? UI_UPDATE : UI_INTERACTIVE +/// Returns a UI status such that advanced tool users will be able to interact, +/// but everyone else can only watch. +/proc/ui_status_user_is_advanced_tool_user(mob/user) + return user.IsAdvancedToolUser() ? UI_INTERACTIVE : UI_UPDATE + +/// Returns a UI status such that silicons will be able to interact with whatever +/// they would have access to if this was a machine. For example, AIs can +/// interact if there's cameras with wireless control is enabled. +/proc/ui_status_silicon_has_access(mob/user, atom/source) + if (!issilicon(user)) + return UI_CLOSE + var/mob/living/silicon/silicon_user = user + return silicon_user.get_ui_access(source) + +/// Returns a UI status representing this silicon's capability to access +/// the given source. Called by `ui_status_silicon_has_access`. +/mob/living/silicon/proc/get_ui_access(atom/source) + return UI_CLOSE + +/mob/living/silicon/robot/get_ui_access(atom/source) + // Robots can interact with anything they can see. + var/list/clientviewlist = getviewsize(client.view) + if(get_dist(src, source) <= min(clientviewlist[1],clientviewlist[2])) + return UI_INTERACTIVE + return UI_DISABLED // Otherwise they can keep the UI open. + +/mob/living/silicon/ai/get_ui_access(atom/source) + // The AI can interact with anything it can see nearby, or with cameras while wireless control is enabled. + if(!control_disabled && can_see(source)) + return UI_INTERACTIVE + return UI_CLOSE + +/mob/living/silicon/pai/get_ui_access(atom/source) + // pAIs can only use themselves and the owner's radio. + if((source == src || source == radio) && !stat) + return UI_INTERACTIVE + else + return UI_CLOSE + +/// Returns UI_INTERACTIVE if the user is conscious and lying down. +/// Returns UI_UPDATE otherwise. +/proc/ui_status_user_is_conscious_and_lying_down(mob/user) + if (!isliving(user)) + return UI_UPDATE + + var/mob/living/living_user = user + return (living_user.lying && living_user.stat == CONSCIOUS) \ + ? UI_INTERACTIVE \ + : UI_UPDATE + +/// Return UI_INTERACTIVE if the user is strictly adjacent to the target atom, whether they can see it or not. +/// Return UI_CLOSE otherwise. +/proc/ui_status_user_strictly_adjacent(mob/user, atom/target) + if(get_dist(target, user) > 1) + return UI_CLOSE + + return UI_INTERACTIVE diff --git a/code/modules/tgui/tgui.dm b/code/modules/tgui/tgui.dm index c739d69d0b..943e0070a5 100644 --- a/code/modules/tgui/tgui.dm +++ b/code/modules/tgui/tgui.dm @@ -92,7 +92,6 @@ window.initialize( fancy = user.client.prefs.tgui_fancy, inline_assets = list( - get_asset_datum(/datum/asset/simple/tgui_common), get_asset_datum(/datum/asset/simple/tgui), )) else @@ -300,7 +299,6 @@ process_status() if(src_object.ui_act(act_type, payload, src, state)) SStgui.update_uis(src_object) - usr?.client?.last_activity = world.time return FALSE switch(type) if("ready") diff --git a/code/modules/tgui/tgui_input_text.dm b/code/modules/tgui/tgui_input_text.dm index a8dcb6a984..dcbc53169e 100644 --- a/code/modules/tgui/tgui_input_text.dm +++ b/code/modules/tgui/tgui_input_text.dm @@ -1,3 +1,7 @@ +/** + * THIS FILE DOES NOT EXIST ON TG, THIS IS PORTED FROM VIRGO. + */ + /** * Creates a TGUI input text window and returns the user's response. * @@ -21,13 +25,14 @@ user = client.mob else return - var/datum/tgui_input_dialog/input = new(user, message, title, default, timeout) - input.input_type = "text" - input.ui_interact(user) - input.wait() - if (input) - . = input.choice - qdel(input) + . = input(user, message, title, default) + // var/datum/tgui_input_dialog/input = new(user, message, title, default, timeout) + // input.input_type = "text" + // input.ui_interact(user) + // input.wait() + // if (input) + // . = input.choice + // qdel(input) /** * Creates a TGUI input message window and returns the user's response. @@ -52,13 +57,15 @@ user = client.mob else return - var/datum/tgui_input_dialog/input = new(user, message, title, default, timeout) - input.input_type = "message" - input.ui_interact(user) - input.wait() - if (input) - . = input.choice - qdel(input) + + . = input(user, message, title, default) + // var/datum/tgui_input_dialog/input = new(user, message, title, default, timeout) + // input.input_type = "message" + // input.ui_interact(user) + // input.wait() + // if (input) + // . = input.choice + // qdel(input) /** * Creates a TGUI input num window and returns the user's response. @@ -83,13 +90,15 @@ user = client.mob else return - var/datum/tgui_input_dialog/input = new(user, message, title, default, timeout) - input.input_type = "num" - input.ui_interact(user) - input.wait() - if (input) - . = input.choice - qdel(input) + + . = input(user, message, title, default) + // var/datum/tgui_input_dialog/input = new(user, message, title, default, timeout) + // input.input_type = "num" + // input.ui_interact(user) + // input.wait() + // if (input) + // . = input.choice + // qdel(input) /** * Creates an asynchronous TGUI input text window with an associated callback. @@ -115,6 +124,7 @@ user = client.mob else return + // . = input(user, message, title, default) var/datum/tgui_input_dialog/async/input = new(user, message, title, default, callback, timeout) input.input_type = "text" input.ui_interact(user) diff --git a/code/modules/tgui_panel/external.dm b/code/modules/tgui_panel/external.dm index 3db779c758..e5b3602e86 100644 --- a/code/modules/tgui_panel/external.dm +++ b/code/modules/tgui_panel/external.dm @@ -16,8 +16,8 @@ nuke_chat() - // Failed to fix - action = tgui_alert(src, "Did that work?", "", list("Yes", "No, switch to old ui")) + // Failed to fix, using tgalert as fallback + action = tgalert(src, "Did that work?", "", "Yes", "No, switch to old ui") if (action == "No, switch to old ui") winset(src, "output", "on-show=&is-disabled=0&is-visible=1") winset(src, "browseroutput", "is-disabled=1;is-visible=0") diff --git a/code/modules/tgui_panel/telemetry.dm b/code/modules/tgui_panel/telemetry.dm index e1abfb1e12..98ba1f14b8 100644 --- a/code/modules/tgui_panel/telemetry.dm +++ b/code/modules/tgui_panel/telemetry.dm @@ -78,3 +78,4 @@ var/msg = "[key_name(client)] has a banned account in connection history! (Matched: [found["ckey"]], [found["address"]], [found["computer_id"]])" message_admins(msg) log_admin_private(msg) + log_suspicious_login(msg, access_log_mirror = FALSE) diff --git a/code/modules/tgui_panel/tgui_panel.dm b/code/modules/tgui_panel/tgui_panel.dm index 895333daee..7e2c8ac61f 100644 --- a/code/modules/tgui_panel/tgui_panel.dm +++ b/code/modules/tgui_panel/tgui_panel.dm @@ -43,7 +43,6 @@ initialized_at = world.time // Perform a clean initialization window.initialize(inline_assets = list( - get_asset_datum(/datum/asset/simple/tgui_common), get_asset_datum(/datum/asset/simple/tgui_panel), )) window.send_asset(get_asset_datum(/datum/asset/simple/namespaced/fontawesome)) diff --git a/code/modules/tooltip/tooltip.dm b/code/modules/tooltip/tooltip.dm index 725eca89db..82ce046540 100644 --- a/code/modules/tooltip/tooltip.dm +++ b/code/modules/tooltip/tooltip.dm @@ -119,42 +119,34 @@ Notes: //Includes sanity checks. /proc/closeToolTip(mob/user) if(istype(user)) - if(user.client) - var/client/client = user.client - if(client.tooltips) - client.tooltips.hide() - deltimer(client.tip_timer) //delete any in-progress timer if the mouse is moved off the item before it finishes - client.tip_timer = null + if(user.client && user.client.tooltips) + user.client.tooltips.hide() + deltimer(user.client.tip_timer) //delete any in-progress timer if the mouse is moved off the item before it finishes + user.client.tip_timer = null /** - * # `get_tooltip_data()` - * * If set, will return a list for the tooltip (that will also be put together in a `Join()`) * However, if returning `null`, the tooltip will not be shown as #14942 changed it. * * Though no tooltips will be created for atoms that have `tooltips = FALSE` */ /atom/movable/proc/get_tooltip_data() - return + return list() /atom/movable/MouseEntered(location, control, params) . = ..() if(tooltips) - if(!QDELETED(src) && usr?.client.prefs.enable_tips) + if((get(src, /mob) == usr && !QDELETED(src)) && usr?.client.prefs.enable_tips) var/list/tooltip_data = get_tooltip_data() if(length(tooltip_data)) var/examine_data = tooltip_data.Join("
") - var/timedelay = max(usr.client.prefs.tip_delay * 0.01, 0.01) // I heard multiplying is faster, also runtimes from very low/negative numbers - usr.client.tip_timer = addtimer(CALLBACK(GLOBAL_PROC, .proc/openToolTip, usr, src, params, name, examine_data), timedelay, TIMER_STOPPABLE) + var/timedelay = usr.client.prefs.tip_delay/100 + usr.client.tip_timer = addtimer(CALLBACK(GLOBAL_PROC, .proc/openToolTip, usr, src, params, name, examine_data), timedelay, TIMER_STOPPABLE)//timer takes delay in deciseconds, but the pref is in milliseconds. dividing by 100 converts it. + +/obj/item/MouseDrop(atom/over, src_location, over_location, src_control, over_control, params) + . = ..() + closeToolTip(usr) /atom/movable/MouseExited(location, control, params) . = ..() closeToolTip(usr) - -/client/MouseDown(object, location, control, params) - closeToolTip(usr) - . = ..() - -/client - /// Timers are now handled by clients, not by doing a mess on the item and multiple people overwriting a single timer on the object, have fun. - var/tip_timer = null diff --git a/code/modules/wiremod/components/abstract/compare.dm b/code/modules/wiremod/components/abstract/compare.dm new file mode 100644 index 0000000000..ef6ba171b7 --- /dev/null +++ b/code/modules/wiremod/components/abstract/compare.dm @@ -0,0 +1,58 @@ +/** + * # Compare Component + * + * Abstract component to build conditional components + */ +/obj/item/circuit_component/compare + display_name = "Compare" + + /// The amount of input ports to have + var/input_port_amount = 4 + + /// The trigger for the true/false signals + var/datum/port/input/compare + + /// Signals sent on compare + var/datum/port/output/true + var/datum/port/output/false + + /// The result from the output + var/datum/port/output/result + + var/list/datum/port/input/compare_ports = list() + +/obj/item/circuit_component/compare/Initialize() + . = ..() + for(var/port_id in 1 to input_port_amount) + var/letter = ascii2text(text2ascii("A") + (port_id-1)) + compare_ports += add_input_port(letter, PORT_TYPE_ANY) + + load_custom_ports() + compare = add_input_port("Compare", PORT_TYPE_SIGNAL) + + true = add_output_port("True", PORT_TYPE_SIGNAL) + false = add_output_port("False", PORT_TYPE_SIGNAL) + result = add_output_port("Result", PORT_TYPE_NUMBER) + +/** + * Used by derivatives to load their own ports in for custom use. + */ +/obj/item/circuit_component/compare/proc/load_custom_ports() + return + +/obj/item/circuit_component/compare/input_received(datum/port/input/port) + . = ..() + if(.) + return + + var/logic_result = do_comparisons(compare_ports) + if(COMPONENT_TRIGGERED_BY(compare, port)) + if(logic_result) + true.set_output(COMPONENT_SIGNAL) + else + false.set_output(COMPONENT_SIGNAL) + result.set_output(logic_result) + +/// Do the comparisons and return a result +/obj/item/circuit_component/compare/proc/do_comparisons(list/ports) + return FALSE diff --git a/code/modules/wiremod/components/abstract/module.dm b/code/modules/wiremod/components/abstract/module.dm new file mode 100644 index 0000000000..a3c04b2a11 --- /dev/null +++ b/code/modules/wiremod/components/abstract/module.dm @@ -0,0 +1,306 @@ +/** + * # Module Component + * + * A component that has an input, output + */ +/obj/item/circuit_component/module + display_name = "Module" + desc = "A component that has other components within it, acting like a function. Use it in your hand to control the amount of input and output ports it has, as well as being able to access the integrated circuit contained inside." + + var/obj/item/integrated_circuit/module/internal_circuit + + var/obj/item/circuit_component/module_input/input_component + var/obj/item/circuit_component/module_output/output_component + + /// Linked ports that follow a `first_port = second_port` keyed structure. + var/list/linked_ports = list() + + var/port_limit = 10 + +/obj/item/integrated_circuit/module + var/obj/item/circuit_component/module/attached_module + +/obj/item/integrated_circuit/module/ui_host(mob/user) + . = ..() + if(. == src) + return attached_module + +/obj/item/integrated_circuit/module/set_display_name(new_name) + . = ..() + attached_module.display_name = new_name + attached_module.name = "module ([new_name])" + +/obj/item/integrated_circuit/module/load_component(type) + if(!attached_module) + return ..() + + if(ispath(type, /obj/item/circuit_component/module_input)) + return attached_module.input_component + + if(ispath(type, /obj/item/circuit_component/module_output)) + return attached_module.output_component + + return ..() + +/obj/item/integrated_circuit/module/Destroy() + attached_module = null + return ..() + +/obj/item/circuit_component/module_input + display_name = "Input" + desc = "A component that receives data from the module it is attached to" + + removable = FALSE + + /// The currently attached module + var/obj/item/circuit_component/module/attached_module + +/obj/item/circuit_component/module_input/Destroy() + attached_module = null + return ..() + +/obj/item/circuit_component/module_output + display_name = "Output" + desc = "A component that outputs data to the module it is attached to." + + removable = FALSE + + /// The currently attached module + var/obj/item/circuit_component/module/attached_module + +/obj/item/circuit_component/module_output/input_received(datum/port/input/port) + . = ..() + if(!port) + return + // We don't check the parent here because frankly, we don't care. We only sync our input with the module's output + var/datum/port/output/port_to_update = attached_module.linked_ports[port] + if(!port_to_update) + CRASH("[port.type] doesn't have a linked port in [type]!") + + port_to_update.set_output(port.value) + +/obj/item/circuit_component/module/input_received(datum/port/input/port) + . = ..() + if(!port) + return + var/datum/port/output/port_to_update = linked_ports[port] + if(!port_to_update) + CRASH("[port.type] doesn't have a linked port in [type]!") + + port_to_update.set_output(port.value) + +/obj/item/circuit_component/module_output/Destroy() + attached_module = null + return ..() + +/obj/item/circuit_component/module/Initialize() + . = ..() + internal_circuit = new(src) + internal_circuit.attached_module = src + + input_component = new(internal_circuit) + input_component.attached_module = src + internal_circuit.add_component(input_component) + input_component.rel_x = 0 + input_component.rel_y = 200 + + output_component = new(internal_circuit) + output_component.attached_module = src + internal_circuit.add_component(output_component) + output_component.rel_x = 400 + output_component.rel_y = 200 + +/obj/item/circuit_component/module/save_data_to_list(list/component_data) + . = ..() + component_data["integrated_circuit"] = internal_circuit.convert_to_json() + + var/list/input_data = list() + for(var/datum/port/input/input_port as anything in input_ports) + input_data += list(list( + "name" = input_port.name, + "type" = input_port.datatype, + )) + + var/list/output_data = list() + for(var/datum/port/output/output_port as anything in output_ports) + output_data += list(list( + "name" = output_port.name, + "type" = output_port.datatype, + )) + + component_data["input_ports"] = input_data + component_data["output_ports"] = output_data + +/obj/item/circuit_component/module/load_data_from_list(list/component_data) + . = ..() + + var/list/input_ports = component_data["input_ports"] + for(var/list/port_data as anything in input_ports) + add_and_link_input_port(port_data["name"], port_data["type"]) + + var/list/output_ports = component_data["output_ports"] + for(var/list/port_data as anything in output_ports) + add_and_link_output_port(port_data["name"], port_data["type"]) + + if(component_data["integrated_circuit"]) + internal_circuit.load_circuit_data(component_data["integrated_circuit"]) + +/obj/item/circuit_component/module/proc/add_and_link_input_port(name, type) + var/datum/port/new_port = add_input_port(name, type) + linked_ports[new_port] = input_component.add_output_port(name, type) + +/obj/item/circuit_component/module/proc/add_and_link_output_port(name, type) + var/datum/port/new_port = output_component.add_input_port(name, type) + linked_ports[new_port] = add_output_port(name, type) + +/obj/item/circuit_component/module/add_to(obj/item/integrated_circuit/added_to) + . = ..() + RegisterSignal(added_to, COMSIG_CIRCUIT_SET_CELL, .proc/handle_set_cell) + RegisterSignal(added_to, COMSIG_CIRCUIT_SET_ON, .proc/handle_set_on) + RegisterSignal(added_to, COMSIG_CIRCUIT_SET_SHELL, .proc/handle_set_shell) + internal_circuit.set_cell(added_to.cell) + internal_circuit.set_shell(added_to.shell) + internal_circuit.set_on(added_to.on) + + +/obj/item/circuit_component/module/removed_from(obj/item/integrated_circuit/removed_from) + internal_circuit.set_cell(null) + internal_circuit.set_on(FALSE) + internal_circuit.remove_current_shell() + UnregisterSignal(removed_from, list( + COMSIG_CIRCUIT_SET_CELL, + COMSIG_CIRCUIT_SET_ON, + COMSIG_CIRCUIT_SET_SHELL, + )) + return ..() + +/obj/item/circuit_component/module/proc/handle_set_cell(datum/source, obj/item/stock_parts/cell/cell) + SIGNAL_HANDLER + internal_circuit.set_cell(cell) + +/obj/item/circuit_component/module/proc/handle_set_on(datum/source, new_value) + SIGNAL_HANDLER + internal_circuit.set_on(new_value) + +/obj/item/circuit_component/module/proc/handle_set_shell(datum/source, atom/movable/new_shell) + SIGNAL_HANDLER + internal_circuit.set_shell(new_shell) + +/obj/item/circuit_component/module/Destroy() + QDEL_NULL(input_component) + QDEL_NULL(output_component) + QDEL_NULL(internal_circuit) + linked_ports = null + return ..() + +/obj/item/circuit_component/module/ui_data(mob/user) + . = list() + .["input_ports"] = list() + for(var/datum/port/input/input_port as anything in input_ports) + .["input_ports"] += list(list( + "name" = input_port.name, + "type" = input_port.datatype, + )) + + .["output_ports"] = list() + for(var/datum/port/output/output_port as anything in output_ports) + .["output_ports"] += list(list( + "name" = output_port.name, + "type" = output_port.datatype, + )) + +/obj/item/circuit_component/module/ui_static_data(mob/user) + . = list() + .["global_port_types"] = GLOB.wiremod_basic_types + +/obj/item/circuit_component/module/attackby(obj/item/I, mob/living/user, params) + if(istype(I, /obj/item/circuit_component)) + internal_circuit.attackby(I, user, params) + return + return ..() + +#define WITHIN_RANGE(id, table) (id >= 1 && id <= length(table)) + +/obj/item/circuit_component/module/ui_act(action, list/params) + . = ..() + if(.) + return + + switch(action) + if("open_internal_circuit") + internal_circuit.interact(usr) + . = TRUE + if("add_input_port") + if(length(input_ports) > port_limit) + return + add_and_link_input_port("Input Port", PORT_TYPE_ANY) + . = TRUE + if("remove_input_port") + var/port_id = text2num(params["port_id"]) + if(!WITHIN_RANGE(port_id, input_ports)) + return + var/datum/port/removed_port = input_ports[port_id] + linked_ports -= removed_port + remove_input_port(removed_port) + input_component.remove_output_port(input_component.output_ports[port_id]) + . = TRUE + if("add_output_port") + if(length(output_ports) > port_limit) + return + add_and_link_output_port("Output Port", PORT_TYPE_ANY) + . = TRUE + if("remove_output_port") + var/port_id = text2num(params["port_id"]) + if(!WITHIN_RANGE(port_id, output_ports)) + return + + var/datum/port/removed_port = output_component.input_ports[port_id] + linked_ports -= removed_port + remove_output_port(output_ports[port_id]) + output_component.remove_input_port(removed_port) + . = TRUE + if("set_port_name", "set_port_type") + var/port_id = text2num(params["port_id"]) + var/is_input = params["is_input"] + + var/list/ports_to_use + var/list/internal_ports_to_use + if(is_input) + ports_to_use = input_ports + internal_ports_to_use = input_component.output_ports + else + ports_to_use = output_ports + internal_ports_to_use = output_component.input_ports + + if(!WITHIN_RANGE(port_id, ports_to_use)) + return + + var/datum/port/component_port = ports_to_use[port_id] + var/datum/port/internal_component_port = internal_ports_to_use[port_id] + + if(action == "set_port_type") + var/type = params["port_type"] + if(!(type in GLOB.wiremod_basic_types)) + return + component_port.set_datatype(type) + internal_component_port.set_datatype(type) + else + var/port_name = params["port_name"] + if(!port_name) + return + port_name = strip_html(port_name, PORT_MAX_NAME_LENGTH) + component_port.name = port_name + internal_component_port.name = port_name + . = TRUE + + if(.) + SStgui.update_uis(internal_circuit) + +#undef WITHIN_RANGE + +/obj/item/circuit_component/module/ui_interact(mob/user, datum/tgui/ui) + ui = SStgui.try_update_ui(user, src, ui) + if(!ui) + ui = new(user, src, "CircuitModule", name) + ui.open() + ui.set_autoupdate(FALSE) diff --git a/code/modules/wiremod/components/action/light.dm b/code/modules/wiremod/components/action/light.dm new file mode 100644 index 0000000000..14427eae0e --- /dev/null +++ b/code/modules/wiremod/components/action/light.dm @@ -0,0 +1,68 @@ +/** + * # Light Component + * + * Emits a light of a specific brightness and colour. Requires a shell. + */ +/obj/item/circuit_component/light + display_name = "Light" + desc = "A component that emits a light of a specific brightness and colour. Requires a shell." + + /// The colours of the light + var/datum/port/input/red + var/datum/port/input/green + var/datum/port/input/blue + + /// The brightness + var/datum/port/input/brightness + + /// Whether the light is on or not + var/datum/port/input/on + + var/max_power = 5 + var/min_lightness = 0.4 + var/shell_light_color + +/obj/item/circuit_component/light/get_ui_notices() + . = ..() + . += create_ui_notice("Maximum Brightness: [max_power]", "orange", "lightbulb") + +/obj/item/circuit_component/light/Initialize() + . = ..() + red = add_input_port("Red", PORT_TYPE_NUMBER) + green = add_input_port("Green", PORT_TYPE_NUMBER) + blue = add_input_port("Blue", PORT_TYPE_NUMBER) + brightness = add_input_port("Brightness", PORT_TYPE_NUMBER) + + on = add_input_port("On", PORT_TYPE_NUMBER) + +/obj/item/circuit_component/light/register_shell(atom/movable/shell) + . = ..() + TRIGGER_CIRCUIT_COMPONENT(src, null) + +/obj/item/circuit_component/light/unregister_shell(atom/movable/shell) + shell.set_light_on(FALSE) + return ..() + +/obj/item/circuit_component/light/input_received(datum/port/input/port) + . = ..() + brightness.set_value(clamp(brightness.value || 0, 0, max_power)) + 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/list/hsl = rgb2hsl(red.value || 0, green.value || 0, blue.value || 0) + var/list/light_col = hsl2rgb(hsl[1], hsl[2], max(min_lightness, hsl[3])) + shell_light_color = rgb(light_col[1], light_col[2], light_col[3]) + if(.) + return + + if(parent.shell) + set_atom_light(parent.shell) + +/obj/item/circuit_component/light/proc/set_atom_light(atom/movable/target_atom) + // Clamp anyways just for safety + var/bright_val = min(max(brightness.value || 0, 0), max_power) + + target_atom.set_light_power(bright_val) + target_atom.set_light_range(bright_val) + target_atom.set_light_color(shell_light_color) + target_atom.set_light_on(!!on.value) diff --git a/code/modules/wiremod/components/action/mmi.dm b/code/modules/wiremod/components/action/mmi.dm new file mode 100644 index 0000000000..9c242059a6 --- /dev/null +++ b/code/modules/wiremod/components/action/mmi.dm @@ -0,0 +1,175 @@ +/** + * # Man-Machine Interface Component + * + * Allows an MMI to be inserted into a shell, allowing it to be linked up. Requires a shell. + */ +/obj/item/circuit_component/mmi + display_name = "Man-Machine Interface" + desc = "A component that allows MMI to enter shells to send output signals." + + /// The message to send to the MMI in the shell. + var/datum/port/input/message + /// Sends the current MMI a message + var/datum/port/input/send + /// Ejects the current MMI + var/datum/port/input/eject + + /// Called when the MMI tries moving north + var/datum/port/output/north + /// Called when the MMI tries moving east + var/datum/port/output/east + /// Called when the MMI tries moving south + var/datum/port/output/south + /// Called when the MMI tries moving west + var/datum/port/output/west + + /// Returns what the MMI last clicked on. + var/datum/port/output/clicked_atom + /// Called when the MMI clicks. + var/datum/port/output/attack + /// Called when the MMI right clicks. + var/datum/port/output/secondary_attack + + /// The current MMI card + var/obj/item/mmi/brain + + /// Maximum length of the message that can be sent to the MMI + var/max_length = 300 + +/obj/item/circuit_component/mmi/Initialize() + . = ..() + message = add_input_port("Message", PORT_TYPE_STRING) + send = add_input_port("Send Message", PORT_TYPE_SIGNAL) + eject = add_input_port("Eject", PORT_TYPE_SIGNAL) + + north = add_output_port("North", PORT_TYPE_SIGNAL) + east = add_output_port("East", PORT_TYPE_SIGNAL) + south = add_output_port("South", PORT_TYPE_SIGNAL) + west = add_output_port("West", PORT_TYPE_SIGNAL) + + attack = add_output_port("Attack", PORT_TYPE_SIGNAL) + secondary_attack = add_output_port("Secondary Attack", PORT_TYPE_SIGNAL) + clicked_atom = add_output_port("Target Entity", PORT_TYPE_ATOM) + +/obj/item/circuit_component/mmi/Destroy() + remove_current_brain() + return ..() + +/obj/item/circuit_component/mmi/input_received(datum/port/input/port) + . = ..() + if(.) + return + + if(!brain) + return + + if(COMPONENT_TRIGGERED_BY(eject, port)) + remove_current_brain() + if(COMPONENT_TRIGGERED_BY(send, port)) + if(!message.value) + return + + var/msg_str = copytext(html_encode(message.value), 1, max_length) + + var/mob/living/target = brain.brainmob + if(!target) + return + + to_chat(target, "[span_bold("You hear a message in your ear: ")][msg_str]") + + +/obj/item/circuit_component/mmi/register_shell(atom/movable/shell) + . = ..() + RegisterSignal(shell, COMSIG_PARENT_ATTACKBY, .proc/handle_attack_by) + +/obj/item/circuit_component/mmi/unregister_shell(atom/movable/shell) + UnregisterSignal(shell, COMSIG_PARENT_ATTACKBY) + remove_current_brain() + return ..() + +/obj/item/circuit_component/mmi/proc/handle_attack_by(atom/movable/shell, obj/item/item, mob/living/attacker) + SIGNAL_HANDLER + if(istype(item, /obj/item/mmi)) + var/obj/item/mmi/target_mmi = item + if(!target_mmi.brainmob) + return + add_mmi(item) + return COMPONENT_NO_AFTERATTACK + +/obj/item/circuit_component/mmi/proc/add_mmi(obj/item/mmi/to_add) + remove_current_brain() + + to_add.forceMove(src) + if(to_add.brainmob) + update_mmi_mob(to_add, null, to_add.brainmob) + brain = to_add + RegisterSignal(to_add, COMSIG_PARENT_QDELETING, .proc/remove_current_brain) + RegisterSignal(to_add, COMSIG_MOVABLE_MOVED, .proc/mmi_moved) + +/obj/item/circuit_component/mmi/proc/mmi_moved(atom/movable/mmi) + SIGNAL_HANDLER + + if(mmi.loc != src) + remove_current_brain() + +/obj/item/circuit_component/mmi/proc/remove_current_brain() + SIGNAL_HANDLER + if(!brain) + return + + if(brain.brainmob) + update_mmi_mob(brain, brain.brainmob) + UnregisterSignal(brain, list( + COMSIG_PARENT_QDELETING, + COMSIG_MOVABLE_MOVED + )) + if(brain.loc == src) + brain.forceMove(drop_location()) + brain = null + +/obj/item/circuit_component/mmi/proc/update_mmi_mob(datum/source, mob/living/old_mmi, mob/living/new_mmi) + SIGNAL_HANDLER + if(old_mmi) + old_mmi.remote_control = null + UnregisterSignal(old_mmi, COMSIG_MOB_CLICKON) + if(new_mmi) + new_mmi.remote_control = src + RegisterSignal(new_mmi, COMSIG_MOB_CLICKON, .proc/handle_mmi_attack) + +/obj/item/circuit_component/mmi/relaymove(mob/living/user, direct) + if(user != brain.brainmob) + return ..() + + if(direct & NORTH) + north.set_output(COMPONENT_SIGNAL) + if(direct & WEST) + west.set_output(COMPONENT_SIGNAL) + if(direct & EAST) + east.set_output(COMPONENT_SIGNAL) + if(direct & SOUTH) + south.set_output(COMPONENT_SIGNAL) + + return TRUE + +/obj/item/circuit_component/mmi/proc/handle_mmi_attack(mob/living/source, atom/target, list/mods) + SIGNAL_HANDLER + var/list/modifiers = params2list(mods) + if(modifiers[RIGHT_CLICK]) + clicked_atom.set_output(target) + secondary_attack.set_output(COMPONENT_SIGNAL) + . = COMSIG_MOB_CANCEL_CLICKON + else if(modifiers[LEFT_CLICK] && !modifiers[SHIFT_CLICK] && !modifiers[ALT_CLICK] && !modifiers[CTRL_CLICK]) + clicked_atom.set_output(target) + attack.set_output(COMPONENT_SIGNAL) + . = COMSIG_MOB_CANCEL_CLICKON + +/obj/item/circuit_component/mmi/add_to(obj/item/integrated_circuit/add_to) + . = ..() + if(HAS_TRAIT(add_to, TRAIT_COMPONENT_MMI)) + return FALSE + ADD_TRAIT(add_to, TRAIT_COMPONENT_MMI, src) + +/obj/item/circuit_component/mmi/removed_from(obj/item/integrated_circuit/removed_from) + REMOVE_TRAIT(removed_from, TRAIT_COMPONENT_MMI, src) + remove_current_brain() + return ..() diff --git a/code/modules/wiremod/components/action/pathfind.dm b/code/modules/wiremod/components/action/pathfind.dm new file mode 100644 index 0000000000..10856e0de8 --- /dev/null +++ b/code/modules/wiremod/components/action/pathfind.dm @@ -0,0 +1,113 @@ +/** + * # Pathfinding component + * + * Calcualtes a path, returns a list of entities. Each entity is the next step in the path. Can be used with the direction component to move. + */ +/obj/item/circuit_component/pathfind + display_name = "Pathfinder" + desc = "When triggered, the next step to the target's location as an entity. This can be used with the direction component and the drone shell to make it move on its own. The Id Card input port is for considering ID access when pathing, it does not give the shell actual access." + circuit_flags = CIRCUIT_FLAG_INPUT_SIGNAL|CIRCUIT_FLAG_OUTPUT_SIGNAL + + var/datum/port/input/input_X + var/datum/port/input/input_Y + var/datum/port/input/id_card + + var/datum/port/output/output + var/datum/port/output/finished + var/datum/port/output/failed + var/datum/port/output/reason_failed + + var/list/path + var/turf/old_dest + var/turf/next_turf + + // Cooldown to limit how frequently we can path to the same location. + var/same_path_cooldown = 5 SECONDS + var/different_path_cooldown = 30 SECONDS + + var/max_range = 60 + +/obj/item/circuit_component/pathfind/get_ui_notices() + . = ..() + // Not necessary to show the same path cooldown, since it doesn't change much for the player + . += create_ui_notice("Pathfinding Cooldown: [DisplayTimeText(different_path_cooldown)]", "orange", "stopwatch") + . += create_ui_notice("Maximum Range: [max_range] tiles", "orange", "info") + +/obj/item/circuit_component/pathfind/Initialize() + . = ..() + input_X = add_input_port("Target X", PORT_TYPE_NUMBER, FALSE) + input_Y = add_input_port("Target Y", PORT_TYPE_NUMBER, FALSE) + id_card = add_input_port("ID Card", PORT_TYPE_ATOM, FALSE) + + output = add_output_port("Next step", PORT_TYPE_ATOM) + finished = add_output_port("Arrived to destination", PORT_TYPE_SIGNAL) + failed = add_output_port("Failed", PORT_TYPE_SIGNAL) + reason_failed = add_output_port("Fail reason", PORT_TYPE_STRING) + +/obj/item/circuit_component/pathfind/input_received(datum/port/input/port) + . = ..() + if(.) + return + + var/target_X = input_X.value + if(isnull(target_X)) + return + + var/target_Y = input_Y.value + if(isnull(target_Y)) + return + + var/atom/path_id = id_card.value + if(path_id && !istype(path_id, /obj/item/card/id)) + path_id = null + failed.set_output(COMPONENT_SIGNAL) + reason_failed.set_output("Object marked is not an ID! Using no ID instead.") + + // Get both the current turf and the destination's turf + var/turf/current_turf = get_turf(src) + var/turf/destination = locate(target_X, target_Y, current_turf?.z) + + // We're already here! No need to do anything. + if(current_turf == destination) + finished.set_output(COMPONENT_SIGNAL) + old_dest = null + TIMER_COOLDOWN_END(parent, COOLDOWN_CIRCUIT_PATHFIND_SAME) + next_turf = null + return + + // If we're going to the same place and the cooldown hasn't subsided, we're probably on the same path as before + if (destination == old_dest && TIMER_COOLDOWN_CHECK(parent, COOLDOWN_CIRCUIT_PATHFIND_SAME)) + + // Check if the current turf is the same as the current turf we're supposed to be in. If so, then we set the next step as the next turf on the list + if(current_turf == next_turf) + popleft(path) + next_turf = get_turf(path[1]) + output.set_output(next_turf) + + // Restart the cooldown since we don't need a new path ( TIMER_COOLDOWN_START might restart the timer by itself and i dont need to call TIMER_COOLDOWN_END, but better safe than sorry ) + TIMER_COOLDOWN_END(parent, COOLDOWN_CIRCUIT_PATHFIND_SAME) + TIMER_COOLDOWN_START(parent, COOLDOWN_CIRCUIT_PATHFIND_SAME, same_path_cooldown) + + + else // Either we're not going to the same place or the cooldown is over. Either way, we need a new path + + if(destination != old_dest && TIMER_COOLDOWN_CHECK(parent, COOLDOWN_CIRCUIT_PATHFIND_DIF)) + failed.set_output(COMPONENT_SIGNAL) + reason_failed.set_output("Cooldown still active!") + return + + TIMER_COOLDOWN_END(parent, COOLDOWN_CIRCUIT_PATHFIND_SAME) + + old_dest = destination + path = get_path_to(src, destination, max_range, id=path_id) + if(length(path) == 0 || !path)// Check if we can even path there + next_turf = null + failed.set_output(COMPONENT_SIGNAL) + reason_failed.set_output("Can't go there!") + return + else + TIMER_COOLDOWN_START(parent, COOLDOWN_CIRCUIT_PATHFIND_DIF, different_path_cooldown) + next_turf = get_turf(path[1]) + output.set_output(next_turf) + TIMER_COOLDOWN_START(parent, COOLDOWN_CIRCUIT_PATHFIND_SAME, same_path_cooldown) + diff --git a/code/modules/wiremod/components/action/pull.dm b/code/modules/wiremod/components/action/pull.dm new file mode 100644 index 0000000000..ecaa6bd3aa --- /dev/null +++ b/code/modules/wiremod/components/action/pull.dm @@ -0,0 +1,31 @@ +/** + * # Pull Component + * + * Tells the shell to start pulling on a designated atom. Only works on movable shells. + */ +/obj/item/circuit_component/pull + display_name = "Start Pulling" + desc = "A component that can force the shell to pull entities. Only works for drone shells." + + /// Frequency input + var/datum/port/input/target + circuit_flags = CIRCUIT_FLAG_INPUT_SIGNAL|CIRCUIT_FLAG_OUTPUT_SIGNAL + +/obj/item/circuit_component/pull/Initialize() + . = ..() + target = add_input_port("Target", PORT_TYPE_ATOM) + +/obj/item/circuit_component/pull/input_received(datum/port/input/port) + . = ..() + if(.) + return + + var/atom/target_atom = target.value + if(!target_atom) + return + + var/mob/shell = parent.shell + if(!istype(shell) || get_dist(shell, target_atom) > 1 || shell.z != target_atom.z) + return + + shell.start_pulling(target_atom) diff --git a/code/modules/wiremod/components/action/radio.dm b/code/modules/wiremod/components/action/radio.dm new file mode 100644 index 0000000000..540b79f7cf --- /dev/null +++ b/code/modules/wiremod/components/action/radio.dm @@ -0,0 +1,74 @@ +#define COMP_RADIO_PUBLIC "public" +#define COMP_RADIO_PRIVATE "private" + +/** + * # Radio Component + * + * Listens out for signals on the designated frequencies and sends signals on designated frequencies + */ +/obj/item/circuit_component/radio + display_name = "Radio" + desc = "A component that can listen and send frequencies. If set to private, the component will only receive signals from other components attached to circuitboards with the same owner id." + + /// The publicity options. Controls whether it's public or private. + var/datum/port/input/option/public_options + + /// Frequency input + var/datum/port/input/freq + /// Signal input + var/datum/port/input/code + + /// Current frequency value + var/current_freq = DEFAULT_SIGNALER_CODE + + var/datum/radio_frequency/radio_connection + +/obj/item/circuit_component/radio/populate_options() + var/static/component_options = list( + COMP_RADIO_PUBLIC, + COMP_RADIO_PRIVATE, + ) + public_options = add_option_port("Encryption Options", component_options) + +/obj/item/circuit_component/radio/Initialize() + . = ..() + freq = add_input_port("Frequency", PORT_TYPE_NUMBER, default = FREQ_SIGNALER) + code = add_input_port("Code", PORT_TYPE_NUMBER, default = DEFAULT_SIGNALER_CODE) + TRIGGER_CIRCUIT_COMPONENT(src, null) + // These are cleaned up on the parent + trigger_input = add_input_port("Send", PORT_TYPE_SIGNAL) + trigger_output = add_output_port("Received", PORT_TYPE_SIGNAL) + +/obj/item/circuit_component/radio/Destroy() + SSradio.remove_object(src, current_freq) + return ..() + +/obj/item/circuit_component/radio/input_received(datum/port/input/port) + . = ..() + freq.set_value(sanitize_frequency(freq.value, TRUE)) + if(.) + return + var/frequency = freq.value + + SSradio.remove_object(src, current_freq) + radio_connection = SSradio.add_object(src, frequency, RADIO_SIGNALER) + current_freq = frequency + + if(COMPONENT_TRIGGERED_BY(trigger_input, port)) + var/datum/signal/signal = new(list("code" = round(code.value) || 0, "key" = parent?.owner_id)) + radio_connection.post_signal(src, signal) + +/obj/item/circuit_component/radio/receive_signal(datum/signal/signal) + . = FALSE + if(!signal) + return + if(signal.data["code"] != round(code.value || 0)) + return + + if(public_options.value == COMP_RADIO_PRIVATE && parent?.owner_id != signal.data["key"]) + return + + trigger_output.set_output(COMPONENT_SIGNAL) + +#undef COMP_RADIO_PUBLIC +#undef COMP_RADIO_PRIVATE diff --git a/code/modules/wiremod/components/action/soundemitter.dm b/code/modules/wiremod/components/action/soundemitter.dm new file mode 100644 index 0000000000..6d18978c53 --- /dev/null +++ b/code/modules/wiremod/components/action/soundemitter.dm @@ -0,0 +1,66 @@ +/** + * # Sound Emitter Component + * + * A component that emits a sound when it receives an input. + */ +/obj/item/circuit_component/soundemitter + display_name = "Sound Emitter" + desc = "A component that emits a sound when it receives an input. The frequency is a multiplier which determines the speed at which the sound is played" + circuit_flags = CIRCUIT_FLAG_INPUT_SIGNAL|CIRCUIT_FLAG_OUTPUT_SIGNAL + + /// Sound to play + var/datum/port/input/option/sound_file + + /// Volume of the sound when played + var/datum/port/input/volume + + /// Frequency of the sound when played + var/datum/port/input/frequency + + /// The cooldown for this component of how often it can play sounds. + var/sound_cooldown = 2 SECONDS + + var/list/options_map + +/obj/item/circuit_component/soundemitter/get_ui_notices() + . = ..() + . += create_ui_notice("Sound Cooldown: [DisplayTimeText(sound_cooldown)]", "orange", "stopwatch") + + +/obj/item/circuit_component/soundemitter/Initialize() + . = ..() + volume = add_input_port("Volume", PORT_TYPE_NUMBER, default = 35) + frequency = add_input_port("Frequency", PORT_TYPE_NUMBER, default = 0) + +/obj/item/circuit_component/soundemitter/populate_options() + var/static/component_options = list( + "Buzz" = 'sound/machines/buzz-sigh.ogg', + "Buzz Twice" = 'sound/machines/buzz-two.ogg', + "Chime" = 'sound/machines/chime.ogg', + "Honk" = 'sound/items/bikehorn.ogg', + "Ping" = 'sound/machines/ping.ogg', + "Sad Trombone" = 'sound/misc/sadtrombone.ogg', + "Warn" = 'sound/machines/warning-buzzer.ogg', + "Slow Clap" = 'sound/machines/slowclap.ogg', + ) + sound_file = add_option_port("Sound Option", component_options) + options_map = component_options + + +/obj/item/circuit_component/soundemitter/input_received(datum/port/input/port) + . = ..() + volume.set_value(clamp(volume.value, 0, 100)) + frequency.set_value(clamp(frequency.value, -100, 100)) + if(.) + return + + if(TIMER_COOLDOWN_CHECK(parent, COOLDOWN_CIRCUIT_SOUNDEMITTER)) + return + + var/sound_to_play = options_map[sound_file.value] + if(!sound_to_play) + return + + playsound(src, sound_to_play, volume.value, frequency != 0, frequency = frequency.value) + + TIMER_COOLDOWN_START(parent, COOLDOWN_CIRCUIT_SOUNDEMITTER, sound_cooldown) diff --git a/code/modules/wiremod/components/action/speech.dm b/code/modules/wiremod/components/action/speech.dm new file mode 100644 index 0000000000..6732a2058a --- /dev/null +++ b/code/modules/wiremod/components/action/speech.dm @@ -0,0 +1,40 @@ +/** + * # Speech Component + * + * Sends a message. Requires a shell. + */ +/obj/item/circuit_component/speech + display_name = "Speech" + desc = "A component that sends a message. Requires a shell." + circuit_flags = CIRCUIT_FLAG_INPUT_SIGNAL|CIRCUIT_FLAG_OUTPUT_SIGNAL + + /// The message to send + var/datum/port/input/message + + /// The cooldown for this component of how often it can send speech messages. + var/speech_cooldown = 1 SECONDS + +/obj/item/circuit_component/speech/get_ui_notices() + . = ..() + . += create_ui_notice("Speech Cooldown: [DisplayTimeText(speech_cooldown)]", "orange", "stopwatch") + +/obj/item/circuit_component/speech/Initialize() + . = ..() + message = add_input_port("Message", PORT_TYPE_STRING, FALSE) + +/obj/item/circuit_component/speech/input_received(datum/port/input/port) + . = ..() + if(.) + return + + if(TIMER_COOLDOWN_CHECK(parent, COOLDOWN_CIRCUIT_SPEECH)) + return + + if(message.value) + var/atom/movable/shell = parent.shell + // Prevents appear as the individual component if there is a shell. + if(shell) + shell.say(message.value) + else + say(message.value) + TIMER_COOLDOWN_START(parent, COOLDOWN_CIRCUIT_SPEECH, speech_cooldown) diff --git a/code/modules/wiremod/components/admin/getvar.dm b/code/modules/wiremod/components/admin/getvar.dm new file mode 100644 index 0000000000..cef8341f38 --- /dev/null +++ b/code/modules/wiremod/components/admin/getvar.dm @@ -0,0 +1,42 @@ +/** + * # Get Variable Component + * + * A component that gets a variable on an object + */ +/obj/item/circuit_component/get_variable + display_name = "Get Variable" + desc = "A component that gets a variable on an object." + circuit_flags = CIRCUIT_FLAG_INPUT_SIGNAL|CIRCUIT_FLAG_OUTPUT_SIGNAL|CIRCUIT_FLAG_ADMIN + + /// Entity to get variable of + var/datum/port/input/entity + + /// Variable name + var/datum/port/input/variable_name + + /// Variable value + var/datum/port/output/output_value + + +/obj/item/circuit_component/get_variable/Initialize() + . = ..() + entity = add_input_port("Target", PORT_TYPE_ATOM) + variable_name = add_input_port("Variable Name", PORT_TYPE_STRING) + + output_value = add_output_port("Output Value", PORT_TYPE_ANY) + +/obj/item/circuit_component/get_variable/input_received(datum/port/input/port) + . = ..() + if(.) + return + var/atom/object = entity.value + var/var_name = variable_name.value + if(!var_name || !object) + output_value.set_output(null) + return + + if(!object.can_vv_get(var_name) || !(var_name in object.vars)) + output_value.set_output(null) + return + + output_value.set_output(object.vars[var_name]) diff --git a/code/modules/wiremod/components/admin/proccall.dm b/code/modules/wiremod/components/admin/proccall.dm new file mode 100644 index 0000000000..1349c81636 --- /dev/null +++ b/code/modules/wiremod/components/admin/proccall.dm @@ -0,0 +1,72 @@ +#define COMP_PROC_GLOBAL "Global" +#define COMP_PROC_OBJECT "Object" + + +/** + * # Proc Call Component + * + * A component that calls a proc on an object and outputs the return value + */ +/obj/item/circuit_component/proccall + display_name = "Proc Call" + desc = "A component that calls a proc on an object." + circuit_flags = CIRCUIT_FLAG_INPUT_SIGNAL|CIRCUIT_FLAG_OUTPUT_SIGNAL|CIRCUIT_FLAG_ADMIN + + var/datum/port/input/option/proccall_options + + /// Entity to proccall on + var/datum/port/input/entity + + /// Proc to call + var/datum/port/input/proc_name + + /// Arguments + var/datum/port/input/arguments + + /// Returns the output from the proccall + var/datum/port/output/output_value + +/obj/item/circuit_component/proccall/populate_options() + var/static/list/component_options = list( + COMP_PROC_OBJECT, + COMP_PROC_GLOBAL, + ) + + proccall_options = add_option_port("Proccall Options", component_options) + +/obj/item/circuit_component/proccall/Initialize() + . = ..() + entity = add_input_port("Target", PORT_TYPE_ATOM) + proc_name = add_input_port("Proc Name", PORT_TYPE_STRING) + arguments = add_input_port("Arguments", PORT_TYPE_LIST) + + output_value = add_output_port("Output Value", PORT_TYPE_ANY) + +/obj/item/circuit_component/proccall/input_received(datum/port/input/port) + . = ..() + if(.) + return + + var/called_on + if(proccall_options.value == COMP_PROC_OBJECT) + called_on = entity.value + else + called_on = GLOBAL_PROC + + if(!called_on) + return + + var/to_invoke = proc_name.value + var/params = arguments.value || list() + + if(!to_invoke) + return + + GLOB.AdminProcCaller = "CHAT_[parent.display_name]" //_ won't show up in ckeys so it'll never match with a real admin + var/result = WrapAdminProcCall(called_on, to_invoke, params) + GLOB.AdminProcCaller = null + + output_value.set_output(result) + +#undef COMP_PROC_GLOBAL +#undef COMP_PROC_OBJECT diff --git a/code/modules/wiremod/components/admin/sdql.dm b/code/modules/wiremod/components/admin/sdql.dm new file mode 100644 index 0000000000..871c3f247e --- /dev/null +++ b/code/modules/wiremod/components/admin/sdql.dm @@ -0,0 +1,36 @@ +/** + * # SDQL Component + * + * A component that performs an sdql operation + */ +/obj/item/circuit_component/sdql_operation + display_name = "SDQL Operation" + desc = "A component that performs an SDQL operation when invoked." + circuit_flags = CIRCUIT_FLAG_INPUT_SIGNAL|CIRCUIT_FLAG_OUTPUT_SIGNAL|CIRCUIT_FLAG_ADMIN + + /// SDQL Operation to invoke + var/datum/port/input/sdql_operation + + var/datum/port/output/results + + +/obj/item/circuit_component/sdql_operation/Initialize() + . = ..() + sdql_operation = add_input_port("SDQL String", PORT_TYPE_STRING) + results = add_output_port("Result", PORT_TYPE_LIST) + +/obj/item/circuit_component/sdql_operation/input_received(datum/port/input/port) + . = ..() + if(.) + return + + var/operation = sdql_operation.value + + if(GLOB.AdminProcCaller || !operation) + return TRUE + + GLOB.AdminProcCaller = "CHAT_[parent.display_name]" //_ won't show up in ckeys so it'll never match with a real admin + var/list/result = world.SDQL2_query(operation, parent.get_creator_admin(), parent.get_creator()) + GLOB.AdminProcCaller = null + + results.set_output(result) diff --git a/code/modules/wiremod/components/admin/setvar.dm b/code/modules/wiremod/components/admin/setvar.dm new file mode 100644 index 0000000000..0c66450cc9 --- /dev/null +++ b/code/modules/wiremod/components/admin/setvar.dm @@ -0,0 +1,36 @@ +/** + * # Set Variable Component + * + * A component that sets a variable on an object + */ +/obj/item/circuit_component/set_variable + display_name = "Set Variable" + desc = "A component that sets a variable on an object." + circuit_flags = CIRCUIT_FLAG_INPUT_SIGNAL|CIRCUIT_FLAG_OUTPUT_SIGNAL|CIRCUIT_FLAG_ADMIN + + /// Entity to set variable of + var/datum/port/input/entity + + /// Variable name + var/datum/port/input/variable_name + + /// New value to set the variable name to. + var/datum/port/input/new_value + + +/obj/item/circuit_component/set_variable/Initialize() + . = ..() + entity = add_input_port("Target", PORT_TYPE_ATOM) + variable_name = add_input_port("Variable Name", PORT_TYPE_STRING) + new_value = add_input_port("New Value", PORT_TYPE_ANY) + +/obj/item/circuit_component/set_variable/input_received(datum/port/input/port) + . = ..() + if(.) + return + var/atom/object = entity.value + var/var_name = variable_name.value + if(!var_name || !object) + return + + object.vv_edit_var(var_name, new_value.value) diff --git a/code/modules/wiremod/components/admin/spawn.dm b/code/modules/wiremod/components/admin/spawn.dm new file mode 100644 index 0000000000..2d3697598b --- /dev/null +++ b/code/modules/wiremod/components/admin/spawn.dm @@ -0,0 +1,47 @@ +/** + * # Spawn Atom Component + * + * Spawns an atom. + */ +/obj/item/circuit_component/spawn_atom + display_name = "Spawn Atom" + desc = "Spawns an atom at a desired location" + circuit_flags = CIRCUIT_FLAG_INPUT_SIGNAL|CIRCUIT_FLAG_OUTPUT_SIGNAL|CIRCUIT_FLAG_ADMIN + + /// The input path to convert into a typepath + var/datum/port/input/input_path + + /// The turf to spawn them at + var/datum/port/input/spawn_at + + /// Parameters to pass to the atom being spawned + var/datum/port/input/parameters + + /// The result from the output + var/datum/port/output/spawned_atom + +/obj/item/circuit_component/spawn_atom/Initialize() + . = ..() + input_path = add_input_port("Type", PORT_TYPE_ANY) + spawn_at = add_input_port("Spawn At", PORT_TYPE_ATOM) + parameters = add_input_port("Parameters", PORT_TYPE_LIST) + + spawned_atom = add_output_port("Spawned Atom", PORT_TYPE_ATOM) + +/obj/item/circuit_component/spawn_atom/input_received(datum/port/input/port) + . = ..() + if(.) + return + + var/typepath = input_path.value + + if(!ispath(typepath, /atom)) + return + + var/list/params = parameters.value + if(!params) + params = list() + + params.Insert(1, spawn_at.value) + + spawned_atom.set_output(new typepath(arglist(params))) diff --git a/code/modules/wiremod/components/admin/to_type.dm b/code/modules/wiremod/components/admin/to_type.dm new file mode 100644 index 0000000000..e4a34223bb --- /dev/null +++ b/code/modules/wiremod/components/admin/to_type.dm @@ -0,0 +1,28 @@ +/** + * # To Type Component + * + * Converts a string into a typepath. Useful for adding components. + */ +/obj/item/circuit_component/to_type + display_name = "String To Type" + desc = "Converts a string into a typepath. Useful for adding components." + circuit_flags = CIRCUIT_FLAG_INPUT_SIGNAL|CIRCUIT_FLAG_OUTPUT_SIGNAL|CIRCUIT_FLAG_ADMIN + + /// The input path to convert into a typepath + var/datum/port/input/input_path + + /// The type output + var/datum/port/output/type_output + +/obj/item/circuit_component/to_type/Initialize() + . = ..() + input_path = add_input_port("Type", PORT_TYPE_STRING) + type_output = add_output_port("Typepath", PORT_TYPE_ANY) + +/obj/item/circuit_component/to_type/input_received(datum/port/input/port) + . = ..() + if(.) + return + + type_output.set_output(text2path(input_path.value)) + diff --git a/code/modules/wiremod/components/atom/direction.dm b/code/modules/wiremod/components/atom/direction.dm new file mode 100644 index 0000000000..140720a2d3 --- /dev/null +++ b/code/modules/wiremod/components/atom/direction.dm @@ -0,0 +1,66 @@ +/** + * # Direction Component + * + * Return the direction of a mob relative to the component + */ +/obj/item/circuit_component/direction + display_name = "Get Direction" + desc = "A component that returns the direction of itself and an entity." + + /// The input port + var/datum/port/input/input_port + + /// The result from the output + var/datum/port/output/output + + // Directions outputs + var/datum/port/output/north + var/datum/port/output/south + var/datum/port/output/east + var/datum/port/output/west + + circuit_flags = CIRCUIT_FLAG_INPUT_SIGNAL|CIRCUIT_FLAG_OUTPUT_SIGNAL + + /// Maximum range for a valid direction to be returned + var/max_range = 7 + +/obj/item/circuit_component/direction/get_ui_notices() + . = ..() + . += create_ui_notice("Maximum Range: [max_range] tiles", "orange", "info") + +/obj/item/circuit_component/direction/Initialize() + . = ..() + input_port = add_input_port("Organism", PORT_TYPE_ATOM) + + output = add_output_port("Direction", PORT_TYPE_STRING) + + north = add_output_port("North", PORT_TYPE_SIGNAL) + east = add_output_port("East", PORT_TYPE_SIGNAL) + south = add_output_port("South", PORT_TYPE_SIGNAL) + west = add_output_port("West", PORT_TYPE_SIGNAL) + +/obj/item/circuit_component/direction/input_received(datum/port/input/port) + . = ..() + if(.) + return + + var/atom/object = input_port.value + if(!object) + return + var/turf/location = get_turf(src) + + if(object.z != location.z || get_dist(location, object) > max_range) + output.set_output(null) + return + + var/direction = get_dir(location, get_turf(object)) + output.set_output(dir2text(direction)) + + if(direction & NORTH) + north.set_output(COMPONENT_SIGNAL) + if(direction & SOUTH) + south.set_output(COMPONENT_SIGNAL) + if(direction & EAST) + east.set_output(COMPONENT_SIGNAL) + if(direction & WEST) + west.set_output(COMPONENT_SIGNAL) diff --git a/code/modules/wiremod/components/atom/gps.dm b/code/modules/wiremod/components/atom/gps.dm new file mode 100644 index 0000000000..cdf61385a0 --- /dev/null +++ b/code/modules/wiremod/components/atom/gps.dm @@ -0,0 +1,34 @@ +/** + * # GPS Component + * + * Return the location of this + */ +/obj/item/circuit_component/gps + display_name = "Internal GPS" + desc = "A component that returns the xyz co-ordinates of itself." + + /// The result from the output + var/datum/port/output/x_pos + var/datum/port/output/y_pos + var/datum/port/output/z_pos + + circuit_flags = CIRCUIT_FLAG_INPUT_SIGNAL|CIRCUIT_FLAG_OUTPUT_SIGNAL + +/obj/item/circuit_component/gps/Initialize() + . = ..() + + x_pos = add_output_port("X", PORT_TYPE_NUMBER) + y_pos = add_output_port("Y", PORT_TYPE_NUMBER) + z_pos = add_output_port("Z", PORT_TYPE_NUMBER) + +/obj/item/circuit_component/gps/input_received(datum/port/input/port) + . = ..() + if(.) + return + + var/turf/location = get_turf(src) + + x_pos.set_output(location?.x) + y_pos.set_output(location?.y) + z_pos.set_output(location?.z) + diff --git a/code/modules/wiremod/components/atom/health.dm b/code/modules/wiremod/components/atom/health.dm new file mode 100644 index 0000000000..7849180f9a --- /dev/null +++ b/code/modules/wiremod/components/atom/health.dm @@ -0,0 +1,62 @@ +/** + * # Get Health Component + * + * Return the health of a mob + */ +/obj/item/circuit_component/health + display_name = "Get Health" + desc = "A component that returns the health of an organism." + + /// The input port + var/datum/port/input/input_port + + /// Brute damage + var/datum/port/output/brute + /// Burn damage + var/datum/port/output/burn + /// Toxin damage + var/datum/port/output/toxin + /// Oxyloss damage + var/datum/port/output/oxy + /// Health + var/datum/port/output/health + + circuit_flags = CIRCUIT_FLAG_INPUT_SIGNAL|CIRCUIT_FLAG_OUTPUT_SIGNAL + + var/max_range = 5 + +/obj/item/circuit_component/health/get_ui_notices() + . = ..() + . += create_ui_notice("Maximum Range: [max_range] tiles", "orange", "info") + +/obj/item/circuit_component/health/Initialize() + . = ..() + input_port = add_input_port("Organism", PORT_TYPE_ATOM) + + brute = add_output_port("Brute Damage", PORT_TYPE_NUMBER) + burn = add_output_port("Burn Damage", PORT_TYPE_NUMBER) + toxin = add_output_port("Toxin Damage", PORT_TYPE_NUMBER) + oxy = add_output_port("Suffocation Damage", PORT_TYPE_NUMBER) + health = add_output_port("Health", PORT_TYPE_NUMBER) + +/obj/item/circuit_component/health/input_received(datum/port/input/port) + . = ..() + if(.) + return + + var/mob/living/organism = input_port.value + var/turf/current_turf = get_turf(src) + if(!istype(organism) || get_dist(current_turf, organism) > max_range || current_turf.z != organism.z) + brute.set_output(null) + burn.set_output(null) + toxin.set_output(null) + oxy.set_output(null) + health.set_output(null) + return + + brute.set_output(organism.getBruteLoss()) + burn.set_output(organism.getFireLoss()) + toxin.set_output(organism.getToxLoss()) + oxy.set_output(organism.getOxyLoss()) + health.set_output(organism.health) + diff --git a/code/modules/wiremod/components/atom/hear.dm b/code/modules/wiremod/components/atom/hear.dm new file mode 100644 index 0000000000..23fbba1e0a --- /dev/null +++ b/code/modules/wiremod/components/atom/hear.dm @@ -0,0 +1,35 @@ +/** + * # Hear Component + * + * Listens for messages. Requires a shell. + */ +/obj/item/circuit_component/hear + display_name = "Voice Activator" + desc = "A component that listens for messages. Requires a shell." + + /// The message heard + var/datum/port/output/message_port + /// The language heard + var/datum/port/output/language_port + /// The speaker + var/datum/port/output/speaker_port + /// The trigger sent when this event occurs + var/datum/port/output/trigger_port + +/obj/item/circuit_component/hear/Initialize() + . = ..() + message_port = add_output_port("Message", PORT_TYPE_STRING) + language_port = add_output_port("Language", PORT_TYPE_STRING) + speaker_port = add_output_port("Speaker", PORT_TYPE_ATOM) + trigger_port = add_output_port("Triggered", PORT_TYPE_SIGNAL) + become_hearing_sensitive(ROUNDSTART_TRAIT) + +/obj/item/circuit_component/hear/Hear(message, atom/movable/speaker, datum/language/message_language, raw_message, radio_freq, list/spans, list/message_mods) + if(speaker == parent?.shell) + return + + message_port.set_output(raw_message) + if(message_language) + language_port.set_output(initial(message_language.name)) + speaker_port.set_output(speaker) + trigger_port.set_output(COMPONENT_SIGNAL) diff --git a/code/modules/wiremod/components/atom/self.dm b/code/modules/wiremod/components/atom/self.dm new file mode 100644 index 0000000000..7389a19f69 --- /dev/null +++ b/code/modules/wiremod/components/atom/self.dm @@ -0,0 +1,21 @@ +/** + * # Self Component + * + * Return the current shell. + */ +/obj/item/circuit_component/self + display_name = "Self" + desc = "A component that returns the current shell." + + /// The shell this component is attached to. + var/datum/port/output/output + +/obj/item/circuit_component/self/Initialize() + . = ..() + output = add_output_port("Self", PORT_TYPE_ATOM) + +/obj/item/circuit_component/self/register_shell(atom/movable/shell) + output.set_output(shell) + +/obj/item/circuit_component/self/unregister_shell(atom/movable/shell) + output.set_output(null) diff --git a/code/modules/wiremod/components/atom/species.dm b/code/modules/wiremod/components/atom/species.dm new file mode 100644 index 0000000000..9404a37697 --- /dev/null +++ b/code/modules/wiremod/components/atom/species.dm @@ -0,0 +1,35 @@ +/** + * # Get Species Component + * + * Return the species of a mob + */ +/obj/item/circuit_component/species + display_name = "Get Species" + desc = "A component that returns the species of its input." + + /// The input port + var/datum/port/input/input_port + + /// The result from the output + var/datum/port/output/output + + circuit_flags = CIRCUIT_FLAG_INPUT_SIGNAL|CIRCUIT_FLAG_OUTPUT_SIGNAL + +/obj/item/circuit_component/species/Initialize() + . = ..() + input_port = add_input_port("Organism", PORT_TYPE_ATOM) + + output = add_output_port("Species", PORT_TYPE_STRING) + +/obj/item/circuit_component/species/input_received(datum/port/input/port) + . = ..() + if(.) + return + + var/mob/living/carbon/human/human = input_port.value + if(!istype(human) || !human.has_dna()) + output.set_output(null) + return + + output.set_output(human.dna.species.name) + diff --git a/code/modules/wiremod/components/hud/bar_overlay.dm b/code/modules/wiremod/components/hud/bar_overlay.dm new file mode 100644 index 0000000000..f7ecfb1e1f --- /dev/null +++ b/code/modules/wiremod/components/hud/bar_overlay.dm @@ -0,0 +1,62 @@ +#define COMP_BAR_OVERLAY_VERTICAL "Vertical" +#define COMP_BAR_OVERLAY_HORIZONTAL "Horizontal" + +/** + * # Bar Overlay Component + * + * Basically an advanced verion of object overlay component that shows a horizontal/vertical bar. + * Requires a BCI shell. + */ + +/obj/item/circuit_component/object_overlay/bar + display_name = "Bar Overlay" + desc = "Requires a BCI shell. A component that shows a bar overlay ontop of an object from a range of 0 to 100." + + var/datum/port/input/option/bar_overlay_options + var/datum/port/input/bar_number + + var/overlay_limit = 10 + +/obj/item/circuit_component/object_overlay/bar/Initialize() + . = ..() + bar_number = add_input_port("Number", PORT_TYPE_NUMBER) + +/obj/item/circuit_component/object_overlay/bar/populate_options() + var/static/component_options_bar = list( + COMP_BAR_OVERLAY_VERTICAL = "barvert", + COMP_BAR_OVERLAY_HORIZONTAL = "barhoriz" + ) + bar_overlay_options = add_option_port("Bar Overlay Options", component_options_bar) + options_map = component_options_bar + +/obj/item/circuit_component/object_overlay/bar/show_to_owner(atom/target_atom, mob/living/owner) + if(LAZYLEN(active_overlays) >= overlay_limit) + return + + var/current_option = bar_overlay_options.value + + if(active_overlays[target_atom]) + QDEL_NULL(active_overlays[target_atom]) + + var/number_clear = clamp(bar_number.value, 0, 100) + if(current_option == COMP_BAR_OVERLAY_HORIZONTAL) + number_clear = round(number_clear / 6.25) * 6.25 + else if(current_option == COMP_BAR_OVERLAY_VERTICAL) + number_clear = round(number_clear / 10) * 10 + var/image/cool_overlay = image(icon = 'icons/hud/screen_bci.dmi', loc = target_atom, icon_state = "[options_map[current_option]][number_clear]", layer = RIPPLE_LAYER) + + if(image_pixel_x.value) + cool_overlay.pixel_x = image_pixel_x.value + + if(image_pixel_y.value) + cool_overlay.pixel_y = image_pixel_y.value + + active_overlays[target_atom] = WEAKREF(target_atom.add_alt_appearance( + /datum/atom_hud/alternate_appearance/basic/one_person, + "bar_overlay_[REF(src)]", + cool_overlay, + owner, + )) + +#undef COMP_BAR_OVERLAY_VERTICAL +#undef COMP_BAR_OVERLAY_HORIZONTAL diff --git a/code/modules/wiremod/components/hud/counter_overlay.dm b/code/modules/wiremod/components/hud/counter_overlay.dm new file mode 100644 index 0000000000..ecce816e74 --- /dev/null +++ b/code/modules/wiremod/components/hud/counter_overlay.dm @@ -0,0 +1,103 @@ +/** + * # Counter Overlay Component + * + * Shows an counter overlay. + * Requires a BCI shell. + */ + +/obj/item/circuit_component/counter_overlay + display_name = "Counter Overlay" + desc = "A component that shows an three digit counter. Requires a BCI shell." + + required_shells = list(/obj/item/organ/cyberimp/bci) + + var/datum/port/input/counter_number + + var/datum/port/input/image_pixel_x + var/datum/port/input/image_pixel_y + + var/datum/port/input/signal_update + + var/obj/item/organ/cyberimp/bci/bci + var/list/numbers = list() + var/counter_appearance + +/obj/item/circuit_component/counter_overlay/Initialize() + . = ..() + counter_number = add_input_port("Displayed Number", PORT_TYPE_NUMBER) + + signal_update = add_input_port("Update Overlay", PORT_TYPE_SIGNAL) + + image_pixel_x = add_input_port("X-Axis Shift", PORT_TYPE_NUMBER) + image_pixel_y = add_input_port("Y-Axis Shift", PORT_TYPE_NUMBER) + +/obj/item/circuit_component/counter_overlay/register_shell(atom/movable/shell) + if(istype(shell, /obj/item/organ/cyberimp/bci)) + bci = shell + RegisterSignal(shell, COMSIG_ORGAN_REMOVED, .proc/on_organ_removed) + +/obj/item/circuit_component/counter_overlay/unregister_shell(atom/movable/shell) + bci = null + QDEL_NULL(counter_appearance) + for(var/number in numbers) + QDEL_NULL(number) + UnregisterSignal(shell, COMSIG_ORGAN_REMOVED) + +/obj/item/circuit_component/counter_overlay/input_received(datum/port/input/port) + . = ..() + + if(. || !bci) + return + + var/mob/living/owner = bci.owner + + if(!owner || !istype(owner) || !owner.client) + return + + for(var/number in numbers) + QDEL_NULL(number) + numbers = list() + + QDEL_NULL(counter_appearance) + var/image/counter = image(icon = 'icons/hud/screen_bci.dmi', icon_state = "hud_numbers", loc = owner) + if(image_pixel_x.value) + counter.pixel_x = image_pixel_x.value + if(image_pixel_y.value) + counter.pixel_y = image_pixel_y.value + + counter_appearance = WEAKREF(owner.add_alt_appearance( + /datum/atom_hud/alternate_appearance/basic/one_person, + "counter_overlay_[REF(src)]", + counter, + owner, + )) + + var/cleared_number = clamp(round(counter_number.value), 0, 999) + + for(var/i = 1 to 3) + var/cur_num = round(cleared_number / (10 ** (3 - i))) % 10 + var/image/number = image(icon = 'icons/hud/screen_bci.dmi', icon_state = "hud_number_[cur_num]", loc = owner) + + if(image_pixel_x.value) + number.pixel_x = image_pixel_x.value + (i - 1) * 9 + if(image_pixel_y.value) + number.pixel_y = image_pixel_y.value + + numbers.Add(WEAKREF(owner.add_alt_appearance( + /datum/atom_hud/alternate_appearance/basic/one_person, + "counter_overlay_[REF(src)]_[i]", + number, + owner, + ))) + +/obj/item/circuit_component/counter_overlay/proc/on_organ_removed(datum/source, mob/living/carbon/owner) + SIGNAL_HANDLER + QDEL_NULL(counter_appearance) + for(var/number in numbers) + QDEL_NULL(number) + +/obj/item/circuit_component/counter_overlay/Destroy() + QDEL_NULL(counter_appearance) + for(var/number in numbers) + QDEL_NULL(number) + return ..() diff --git a/code/modules/wiremod/components/hud/object_overlay.dm b/code/modules/wiremod/components/hud/object_overlay.dm new file mode 100644 index 0000000000..2cab4a57fa --- /dev/null +++ b/code/modules/wiremod/components/hud/object_overlay.dm @@ -0,0 +1,122 @@ +/** + * # Object Overlay Component + * + * Shows an overlay ontop of an object. Toggleable. + * Requires a BCI shell. + */ + +#define OBJECT_OVERLAY_LIMIT 10 + +/obj/item/circuit_component/object_overlay + display_name = "Object Overlay" + desc = "Requires a BCI shell. A component that shows an overlay on top of an object." + + required_shells = list(/obj/item/organ/cyberimp/bci) + + var/datum/port/input/option/object_overlay_options + + /// Target atom + var/datum/port/input/target + + var/datum/port/input/image_pixel_x + var/datum/port/input/image_pixel_y + + /// On/Off signals + var/datum/port/input/signal_on + var/datum/port/input/signal_off + + var/obj/item/organ/cyberimp/bci/bci + var/list/active_overlays = list() + var/list/options_map + +/obj/item/circuit_component/object_overlay/Initialize() + . = ..() + target = add_input_port("Target", PORT_TYPE_ATOM) + + signal_on = add_input_port("Create Overlay", PORT_TYPE_SIGNAL) + signal_off = add_input_port("Remove Overlay", PORT_TYPE_SIGNAL) + + image_pixel_x = add_input_port("X-Axis Shift", PORT_TYPE_NUMBER) + image_pixel_y = add_input_port("Y-Axis Shift", PORT_TYPE_NUMBER) + +/obj/item/circuit_component/object_overlay/Destroy() + for(var/active_overlay in active_overlays) + QDEL_NULL(active_overlay) + return ..() + +/obj/item/circuit_component/object_overlay/populate_options() + var/static/component_options = list( + "Corners (Blue)" = "hud_corners", + "Corners (Red)" = "hud_corners_red", + "Circle (Blue)" = "hud_circle", + "Circle (Red)" = "hud_circle_red", + "Small Corners (Blue)" = "hud_corners_small", + "Small Corners (Red)" = "hud_corners_small_red", + "Triangle (Blue)" = "hud_triangle", + "Triangle (Red)" = "hud_triangle_red", + "HUD mark (Blue)" = "hud_mark", + "HUD mark (Red)" = "hud_mark_red" + ) + object_overlay_options = add_option_port("Object", component_options) + options_map = component_options + +/obj/item/circuit_component/object_overlay/register_shell(atom/movable/shell) + if(istype(shell, /obj/item/organ/cyberimp/bci)) + bci = shell + RegisterSignal(shell, COMSIG_ORGAN_REMOVED, .proc/on_organ_removed) + +/obj/item/circuit_component/object_overlay/unregister_shell(atom/movable/shell) + bci = null + UnregisterSignal(shell, COMSIG_ORGAN_REMOVED) + +/obj/item/circuit_component/object_overlay/input_received(datum/port/input/port) + . = ..() + + if(. || !bci) + return + + var/mob/living/owner = bci.owner + var/atom/target_atom = target.value + + if(!owner || !istype(owner) || !owner.client || !target_atom) + return + + if(COMPONENT_TRIGGERED_BY(signal_on, port)) + show_to_owner(target_atom, owner) + + if(COMPONENT_TRIGGERED_BY(signal_off, port) && (target_atom in active_overlays)) + QDEL_NULL(active_overlays[target_atom]) + active_overlays.Remove(target_atom) + +/obj/item/circuit_component/object_overlay/proc/show_to_owner(atom/target_atom, mob/living/owner) + if(LAZYLEN(active_overlays) >= OBJECT_OVERLAY_LIMIT) + return + + if(active_overlays[target_atom]) + QDEL_NULL(active_overlays[target_atom]) + + var/image/cool_overlay = image(icon = 'icons/hud/screen_bci.dmi', loc = target_atom, icon_state = options_map[object_overlay_options.value], layer = RIPPLE_LAYER) + + if(image_pixel_x.value) + cool_overlay.pixel_x = image_pixel_x.value + + if(image_pixel_y.value) + cool_overlay.pixel_y = image_pixel_y.value + + var/alt_appearance = WEAKREF(target_atom.add_alt_appearance( + /datum/atom_hud/alternate_appearance/basic/one_person, + "object_overlay_[REF(src)]", + cool_overlay, + owner, + )) + + active_overlays[target_atom] = alt_appearance + +/obj/item/circuit_component/object_overlay/proc/on_organ_removed(datum/source, mob/living/carbon/owner) + SIGNAL_HANDLER + + for(var/atom/target_atom in active_overlays) + QDEL_NULL(active_overlays[target_atom]) + active_overlays.Remove(target_atom) + +#undef OBJECT_OVERLAY_LIMIT diff --git a/code/modules/wiremod/components/hud/target_intercept.dm b/code/modules/wiremod/components/hud/target_intercept.dm new file mode 100644 index 0000000000..2a19264b00 --- /dev/null +++ b/code/modules/wiremod/components/hud/target_intercept.dm @@ -0,0 +1,64 @@ +/** + * # Target Intercept Component + * + * When activated intercepts next click and outputs clicked atom. + * Requires a BCI shell. + */ + +/obj/item/circuit_component/target_intercept + display_name = "Target Intercept" + desc = "Requires a BCI shell. When activated, this component will allow user to target an object using their brain and will output the reference to said object." + + required_shells = list(/obj/item/organ/cyberimp/bci) + + var/datum/port/output/clicked_atom + + var/obj/item/organ/cyberimp/bci/bci + var/intercept_cooldown = 1 SECONDS + +/obj/item/circuit_component/target_intercept/Initialize() + . = ..() + trigger_input = add_input_port("Activate", PORT_TYPE_SIGNAL) + trigger_output = add_output_port("Triggered", PORT_TYPE_SIGNAL) + clicked_atom = add_output_port("Targeted Object", PORT_TYPE_ATOM) + +/obj/item/circuit_component/target_intercept/register_shell(atom/movable/shell) + if(istype(shell, /obj/item/organ/cyberimp/bci)) + bci = shell + RegisterSignal(shell, COMSIG_ORGAN_REMOVED, .proc/on_organ_removed) + +/obj/item/circuit_component/target_intercept/unregister_shell(atom/movable/shell) + bci = null + UnregisterSignal(shell, COMSIG_ORGAN_REMOVED) + +/obj/item/circuit_component/target_intercept/input_received(datum/port/input/port) + . = ..() + + if(. || !bci) + return + + var/mob/living/owner = bci.owner + if(!owner || !istype(owner) || !owner.client) + return + + if(TIMER_COOLDOWN_CHECK(parent, COOLDOWN_CIRCUIT_TARGET_INTERCEPT)) + return + + to_chat(owner, "Left-click to trigger target interceptor!") + owner.client.click_intercept = src + +/obj/item/circuit_component/target_intercept/proc/on_organ_removed(datum/source, mob/living/carbon/owner) + SIGNAL_HANDLER + + if(owner.client && owner.client.click_intercept == src) + owner.client.click_intercept = null + +/obj/item/circuit_component/target_intercept/proc/InterceptClickOn(mob/user, params, atom/object) + user.client.click_intercept = null + clicked_atom.set_output(object) + trigger_output.set_output(COMPONENT_SIGNAL) + TIMER_COOLDOWN_START(parent, COOLDOWN_CIRCUIT_TARGET_INTERCEPT, intercept_cooldown) + +/obj/item/circuit_component/target_intercept/get_ui_notices() + . = ..() + . += create_ui_notice("Target Interception Cooldown: [DisplayTimeText(intercept_cooldown)]", "orange", "stopwatch") diff --git a/code/modules/wiremod/components/list/concat.dm b/code/modules/wiremod/components/list/concat.dm new file mode 100644 index 0000000000..32e16758d3 --- /dev/null +++ b/code/modules/wiremod/components/list/concat.dm @@ -0,0 +1,48 @@ +/** + * # Concat List Component + * + * Concatenates a list with a separator + */ +/obj/item/circuit_component/concat_list + display_name = "Concatenate List" + desc = "A component that joins up a list with a separator into a single string." + + /// The input port + var/datum/port/input/list_port + + /// The seperator + var/datum/port/input/separator + + /// The result from the output + var/datum/port/output/output + circuit_flags = CIRCUIT_FLAG_INPUT_SIGNAL|CIRCUIT_FLAG_OUTPUT_SIGNAL + +/obj/item/circuit_component/concat_list/Initialize() + . = ..() + list_port = add_input_port("List", PORT_TYPE_LIST) + separator = add_input_port("Seperator", PORT_TYPE_STRING) + + output = add_output_port("Output", PORT_TYPE_STRING) + +/obj/item/circuit_component/concat_list/input_received(datum/port/input/port) + . = ..() + if(.) + return + + var/seperator = separator.value + if(!seperator) + return + + var/list/list_input = list_port.value + if(!list_input) + return + + var/list/text_list = list() + for(var/entry in list_input) + if(isatom(entry)) + text_list += PORT_TYPE_ATOM + else + text_list += "[entry]" + + output.set_output(text_list.Join(seperator)) + diff --git a/code/modules/wiremod/components/list/get_column.dm b/code/modules/wiremod/components/list/get_column.dm new file mode 100644 index 0000000000..07ff7ef11b --- /dev/null +++ b/code/modules/wiremod/components/list/get_column.dm @@ -0,0 +1,42 @@ +/** + * # Get Column Component + * + * Gets the column of a table and returns it as a regular list. + */ +/obj/item/circuit_component/get_column + display_name = "Get Column" + desc = "Gets the column of a table and returns it as a regular list." + circuit_flags = CIRCUIT_FLAG_INPUT_SIGNAL|CIRCUIT_FLAG_OUTPUT_SIGNAL + + /// The list to perform the filter on + var/datum/port/input/received_table + + /// The name of the column to check + var/datum/port/input/column_name + + /// The filtered list + var/datum/port/output/output_list + +/obj/item/circuit_component/get_column/Initialize() + . = ..() + received_table = add_input_port("Input", PORT_TYPE_TABLE) + column_name = add_input_port("Column Name", PORT_TYPE_STRING) + output_list = add_output_port("Output", PORT_TYPE_LIST) + +/obj/item/circuit_component/get_column/input_received(datum/port/input/port) + . = ..() + if(.) + return + + var/list/input_list = received_table.value + if(!islist(input_list) || isnum(column_name.value)) + return + + var/list/new_list = list() + for(var/list/entry in input_list) + var/anything = entry[column_name.value] + if(islist(anything)) + continue + new_list += anything + + output_list.set_output(new_list) diff --git a/code/modules/wiremod/components/list/index.dm b/code/modules/wiremod/components/list/index.dm new file mode 100644 index 0000000000..d51a60e2bf --- /dev/null +++ b/code/modules/wiremod/components/list/index.dm @@ -0,0 +1,42 @@ +/** + * # Index Component + * + * Return the index of a list + */ +/obj/item/circuit_component/index + display_name = "Index List" + desc = "A component that returns the value of a list at a given index." + + /// The input port + var/datum/port/input/list_port + var/datum/port/input/index_port + + /// The result from the output + var/datum/port/output/output + circuit_flags = CIRCUIT_FLAG_INPUT_SIGNAL|CIRCUIT_FLAG_OUTPUT_SIGNAL + +/obj/item/circuit_component/index/Initialize() + . = ..() + index_port = add_input_port("Index", PORT_TYPE_ANY) + list_port = add_input_port("List", PORT_TYPE_LIST) + + output = add_output_port("Value", PORT_TYPE_ANY) + +/obj/item/circuit_component/index/input_received(datum/port/input/port) + . = ..() + if(.) + return + + var/index = index_port.value + var/list/list_input = list_port.value + + if(!islist(list_input) || !index) + output.set_output(null) + return + + if(isnum(index) && (index < 1 || index > length(list_input))) + output.set_output(null) + return + + output.set_output(list_input[index]) + diff --git a/code/modules/wiremod/components/list/index_table.dm b/code/modules/wiremod/components/list/index_table.dm new file mode 100644 index 0000000000..376bc46465 --- /dev/null +++ b/code/modules/wiremod/components/list/index_table.dm @@ -0,0 +1,42 @@ +/** + * # Index Table Component + * + * Gets the row of a table using the index inputted. Will return no value if the index is invalid or a proper table is not returned. + */ +/obj/item/circuit_component/index_table + display_name = "Index Table" + desc = "Gets the row of a table using the index inputted. Will return no value if the index is invalid or a proper table is not returned." + circuit_flags = CIRCUIT_FLAG_INPUT_SIGNAL|CIRCUIT_FLAG_OUTPUT_SIGNAL + + /// The list to perform the filter on + var/datum/port/input/received_table + + /// The target index + var/datum/port/input/target_index + + /// The filtered list + var/datum/port/output/output_list + +/obj/item/circuit_component/index_table/Initialize() + . = ..() + received_table = add_input_port("Input", PORT_TYPE_TABLE) + target_index = add_input_port("Index", PORT_TYPE_NUMBER) + + output_list = add_output_port("Output", PORT_TYPE_LIST) + +/obj/item/circuit_component/index_table/input_received(datum/port/input/port) + . = ..() + if(.) + return + + var/list/target_list = received_table.value + if(!islist(target_list) || !length(target_list)) + output_list.set_output(null) + return + + var/index = target_index.value + if(index < 1 || index > length(target_list)) + output_list.set_output(null) + return + + output_list.set_output(target_list[index]) diff --git a/code/modules/wiremod/components/list/list_literal.dm b/code/modules/wiremod/components/list/list_literal.dm new file mode 100644 index 0000000000..e9bd741606 --- /dev/null +++ b/code/modules/wiremod/components/list/list_literal.dm @@ -0,0 +1,86 @@ +/** + * # List Literal Component + * + * Return a list literal. + */ +/obj/item/circuit_component/list_literal + display_name = "List Literal" + desc = "A component that returns the value of a list at a given index. Attack in hand to increase list size, right click to decrease list size." + circuit_flags = CIRCUIT_FLAG_INPUT_SIGNAL|CIRCUIT_FLAG_OUTPUT_SIGNAL + + /// The inputs used to create the list + var/list/datum/port/input/entry_ports = list() + /// The result from the output + var/datum/port/output/list_output + + var/length = 0 + + var/default_list_size = 2 + + var/min_size = 1 + var/max_size = 20 + +/obj/item/circuit_component/list_literal/save_data_to_list(list/component_data) + . = ..() + component_data["length"] = length + +/obj/item/circuit_component/list_literal/load_data_from_list(list/component_data) + set_list_size(component_data["length"]) + + return ..() + +/obj/item/circuit_component/list_literal/proc/set_list_size(new_size) + if(new_size <= 0) + for(var/datum/port/input/port in entry_ports) + remove_input_port(port) + entry_ports = list() + length = 0 + return + + while(length > new_size) + var/index = length(entry_ports) + var/entry_port = entry_ports[index] + entry_ports -= entry_port + remove_input_port(entry_port) + length-- + + while(length < new_size) + length++ + var/index = length(input_ports) + if(trigger_input) + index -= 1 + entry_ports += add_input_port("Index [index+1]", PORT_TYPE_ANY, index = index+1) + +/obj/item/circuit_component/list_literal/Initialize() + . = ..() + set_list_size(default_list_size) + list_output = add_output_port("Value", PORT_TYPE_LIST) + +/obj/item/circuit_component/list_literal/Destroy() + list_output = null + return ..() + +// Increases list length +/obj/item/circuit_component/list_literal/attack_self(mob/user, list/modifiers) + . = ..() + set_list_size(min(length + 1, max_size)) + balloon_alert(user, "new size is now [length]") + +// Decreases list length +/obj/item/circuit_component/list_literal/attack_self_secondary(mob/user, list/modifiers) + . = ..() + set_list_size(max(length - 1, min_size)) + balloon_alert(user, "new size is now [length]") + +/obj/item/circuit_component/list_literal/input_received(datum/port/input/port) + . = ..() + if(.) + return + + var/list/new_literal = list() + for(var/datum/port/input/entry_port as anything in entry_ports) + // Prevents lists from merging together + new_literal += list(entry_port.value) + + list_output.set_output(new_literal) + diff --git a/code/modules/wiremod/components/list/select.dm b/code/modules/wiremod/components/list/select.dm new file mode 100644 index 0000000000..8eba84d034 --- /dev/null +++ b/code/modules/wiremod/components/list/select.dm @@ -0,0 +1,93 @@ +/** + * # Select Component + * + * Selects a list from a list of lists by a specific column. Used only by USBs for communications to and from computers with lists of varying sizes. + */ +/obj/item/circuit_component/select + display_name = "Select Query" + desc = "A component used with USB cables that can perform select queries on a list based on the column name selected. The values are then compared with the comparison input." + circuit_flags = CIRCUIT_FLAG_INPUT_SIGNAL|CIRCUIT_FLAG_OUTPUT_SIGNAL + + var/datum/port/input/option/comparison_options + + /// The list to perform the filter on + var/datum/port/input/received_table + + /// The name of the column to check + var/datum/port/input/column_name + + /// The input to compare with + var/datum/port/input/comparison_input + + /// The filtered list + var/datum/port/output/filtered_table + + var/current_type = PORT_TYPE_ANY + +/obj/item/circuit_component/select/populate_options() + var/static/component_options = list( + COMP_COMPARISON_EQUAL, + COMP_COMPARISON_NOT_EQUAL, + COMP_COMPARISON_GREATER_THAN, + COMP_COMPARISON_LESS_THAN, + COMP_COMPARISON_GREATER_THAN_OR_EQUAL, + COMP_COMPARISON_LESS_THAN_OR_EQUAL, + ) + comparison_options = add_option_port("Comparison Options", component_options) + +/obj/item/circuit_component/select/Initialize() + . = ..() + received_table = add_input_port("Input", PORT_TYPE_TABLE) + column_name = add_input_port("Column Name", PORT_TYPE_STRING) + comparison_input = add_input_port("Comparison Input", PORT_TYPE_ANY) + + filtered_table = add_output_port("Output", PORT_TYPE_TABLE) + +/obj/item/circuit_component/select/input_received(datum/port/input/port) + . = ..() + var/current_option = comparison_options.value + + switch(current_option) + if(COMP_COMPARISON_EQUAL, COMP_COMPARISON_NOT_EQUAL) + if(current_type != PORT_TYPE_ANY) + current_type = PORT_TYPE_ANY + comparison_input.set_datatype(PORT_TYPE_ANY) + else + if(current_type != PORT_TYPE_NUMBER) + current_type = PORT_TYPE_NUMBER + comparison_input.set_datatype(PORT_TYPE_NUMBER) + + if(.) + return + + var/list/input_list = received_table.value + if(!islist(input_list) || isnum(column_name.value)) + return + + var/comparison_value = comparison_input.value + var/list/new_list = list() + for(var/list/entry in input_list) + var/anything = entry[column_name.value] + if(islist(anything)) + continue + if(current_option != COMP_COMPARISON_EQUAL && current_option != COMP_COMPARISON_NOT_EQUAL && !isnum(anything)) + continue + var/add_to_list = FALSE + switch(current_option) + if(COMP_COMPARISON_EQUAL) + add_to_list = anything == comparison_value + if(COMP_COMPARISON_NOT_EQUAL) + add_to_list = anything != comparison_value + if(COMP_COMPARISON_GREATER_THAN) + add_to_list = anything > comparison_value + if(COMP_COMPARISON_GREATER_THAN_OR_EQUAL) + add_to_list = anything >= comparison_value + if(COMP_COMPARISON_LESS_THAN) + add_to_list = anything < comparison_value + if(COMP_COMPARISON_LESS_THAN_OR_EQUAL) + add_to_list = anything <= comparison_value + + if(add_to_list) + new_list += list(entry) + + filtered_table.set_output(new_list) diff --git a/code/modules/wiremod/components/list/split.dm b/code/modules/wiremod/components/list/split.dm new file mode 100644 index 0000000000..27af366fac --- /dev/null +++ b/code/modules/wiremod/components/list/split.dm @@ -0,0 +1,42 @@ +/** + * # Split component + * + * Splits a string + */ +/obj/item/circuit_component/split + display_name = "Split" + desc = "Splits a string by the separator, turning it into a list" + + /// The input port + var/datum/port/input/input_port + + /// The seperator + var/datum/port/input/separator + + /// The result from the output + var/datum/port/output/output + + circuit_flags = CIRCUIT_FLAG_INPUT_SIGNAL|CIRCUIT_FLAG_OUTPUT_SIGNAL + +/obj/item/circuit_component/split/Initialize() + . = ..() + input_port = add_input_port("Input", PORT_TYPE_STRING) + separator = add_input_port("Seperator", PORT_TYPE_STRING) + output = add_output_port("Output", PORT_TYPE_LIST) + +/obj/item/circuit_component/split/input_received(datum/port/input/port) + . = ..() + if(.) + return + + var/separator_value = separator.value + if(isnull(separator_value)) + return + + var/value = input_port.value + if(isnull(value)) + return + + var/list/result = splittext(value,separator_value) + + output.set_output(result) diff --git a/code/modules/wiremod/components/math/arithmetic.dm b/code/modules/wiremod/components/math/arithmetic.dm new file mode 100644 index 0000000000..77c8b93b25 --- /dev/null +++ b/code/modules/wiremod/components/math/arithmetic.dm @@ -0,0 +1,88 @@ +#define COMP_ARITHMETIC_ADD "Add" +#define COMP_ARITHMETIC_SUBTRACT "Subtract" +#define COMP_ARITHMETIC_MULTIPLY "Multiply" +#define COMP_ARITHMETIC_DIVIDE "Divide" +#define COMP_ARITHMETIC_MIN "Minimum" +#define COMP_ARITHMETIC_MAX "Maximum" + +/** + * # Arithmetic Component + * + * General arithmetic unit with add/sub/mult/divide capabilities + * This one only works with numbers. + */ +/obj/item/circuit_component/arithmetic + display_name = "Arithmetic" + desc = "General arithmetic component with arithmetic capabilities." + + /// The amount of input ports to have + var/input_port_amount = 4 + + var/datum/port/input/option/arithmetic_option + + /// The result from the output + var/datum/port/output/output + + var/list/arithmetic_ports + circuit_flags = CIRCUIT_FLAG_INPUT_SIGNAL|CIRCUIT_FLAG_OUTPUT_SIGNAL + +/obj/item/circuit_component/arithmetic/populate_options() + var/static/component_options = list( + COMP_ARITHMETIC_ADD, + COMP_ARITHMETIC_SUBTRACT, + COMP_ARITHMETIC_MULTIPLY, + COMP_ARITHMETIC_DIVIDE, + COMP_ARITHMETIC_MIN, + COMP_ARITHMETIC_MAX, + ) + arithmetic_option = add_option_port("Arithmetic Option", component_options) + +/obj/item/circuit_component/arithmetic/Initialize() + . = ..() + arithmetic_ports = list() + for(var/port_id in 1 to input_port_amount) + var/letter = ascii2text(text2ascii("A") + (port_id-1)) + arithmetic_ports += add_input_port(letter, PORT_TYPE_NUMBER) + + output = add_output_port("Output", PORT_TYPE_NUMBER) + +/obj/item/circuit_component/arithmetic/input_received(datum/port/input/port) + . = ..() + if(.) + return + + var/list/ports = arithmetic_ports.Copy() + var/datum/port/input/first_port = popleft(ports) + var/result = first_port.value + + for(var/datum/port/input/input_port as anything in ports) + var/value = input_port.value + if(isnull(value)) + continue + + switch(arithmetic_option.value) + if(COMP_ARITHMETIC_ADD) + result += value + if(COMP_ARITHMETIC_SUBTRACT) + result -= value + if(COMP_ARITHMETIC_MULTIPLY) + result *= value + if(COMP_ARITHMETIC_DIVIDE) + // Protect from div by zero errors. + if(value == 0) + result = null + break + result /= value + if(COMP_ARITHMETIC_MAX) + result = max(result, value) + if(COMP_ARITHMETIC_MIN) + result = min(result, value) + + output.set_output(result) + +#undef COMP_ARITHMETIC_ADD +#undef COMP_ARITHMETIC_SUBTRACT +#undef COMP_ARITHMETIC_MULTIPLY +#undef COMP_ARITHMETIC_DIVIDE +#undef COMP_ARITHMETIC_MIN +#undef COMP_ARITHMETIC_MAX diff --git a/code/modules/wiremod/components/math/comparison.dm b/code/modules/wiremod/components/math/comparison.dm new file mode 100644 index 0000000000..0415b04568 --- /dev/null +++ b/code/modules/wiremod/components/math/comparison.dm @@ -0,0 +1,62 @@ +/** + * # Comparison Component + * + * Compares two objects + */ +/obj/item/circuit_component/compare/comparison + display_name = "Comparison" + desc = "A component that compares two objects." + + var/datum/port/input/option/comparison_option + + input_port_amount = 2 + var/current_type = PORT_TYPE_ANY + +/obj/item/circuit_component/compare/comparison/populate_options() + var/static/component_options = list( + COMP_COMPARISON_EQUAL, + COMP_COMPARISON_NOT_EQUAL, + COMP_COMPARISON_GREATER_THAN, + COMP_COMPARISON_LESS_THAN, + COMP_COMPARISON_GREATER_THAN_OR_EQUAL, + COMP_COMPARISON_LESS_THAN_OR_EQUAL, + ) + comparison_option = add_option_port("Comparison Option", component_options) + +/obj/item/circuit_component/compare/comparison/input_received(datum/port/input/port) + switch(comparison_option.value) + if(COMP_COMPARISON_EQUAL, COMP_COMPARISON_NOT_EQUAL) + if(current_type != PORT_TYPE_ANY) + current_type = PORT_TYPE_ANY + compare_ports[1].set_datatype(PORT_TYPE_ANY) + compare_ports[2].set_datatype(PORT_TYPE_ANY) + else + if(current_type != PORT_TYPE_NUMBER) + current_type = PORT_TYPE_NUMBER + compare_ports[1].set_datatype(PORT_TYPE_NUMBER) + compare_ports[2].set_datatype(PORT_TYPE_NUMBER) + return ..() + + +/obj/item/circuit_component/compare/comparison/do_comparisons(list/ports) + if(length(ports) < input_port_amount) + return FALSE + + // Comparison component only compares the first two ports + var/input1 = compare_ports[1].value + var/input2 = compare_ports[2].value + var/current_option = comparison_option.value + + switch(current_option) + if(COMP_COMPARISON_EQUAL) + return input1 == input2 + if(COMP_COMPARISON_NOT_EQUAL) + return input1 != input2 + if(COMP_COMPARISON_GREATER_THAN) + return input1 > input2 + if(COMP_COMPARISON_GREATER_THAN_OR_EQUAL) + return input1 >= input2 + if(COMP_COMPARISON_LESS_THAN) + return input1 < input2 + if(COMP_COMPARISON_LESS_THAN_OR_EQUAL) + return input1 <= input2 diff --git a/code/modules/wiremod/components/math/length.dm b/code/modules/wiremod/components/math/length.dm new file mode 100644 index 0000000000..d6702c9b5d --- /dev/null +++ b/code/modules/wiremod/components/math/length.dm @@ -0,0 +1,29 @@ +/** + * # Length Component + * + * Return the length of an input + */ +/obj/item/circuit_component/length + display_name = "Length" + desc = "A component that returns the length of its input." + + /// The input port + var/datum/port/input/input_port + + /// The result from the output + var/datum/port/output/output + circuit_flags = CIRCUIT_FLAG_INPUT_SIGNAL|CIRCUIT_FLAG_OUTPUT_SIGNAL + +/obj/item/circuit_component/length/Initialize() + . = ..() + input_port = add_input_port("Input", PORT_TYPE_ANY) + + output = add_output_port("Length", PORT_TYPE_NUMBER) + +/obj/item/circuit_component/length/input_received(datum/port/input/port) + . = ..() + if(.) + return + + output.set_output(length(input_port.value)) + diff --git a/code/modules/wiremod/components/math/logic.dm b/code/modules/wiremod/components/math/logic.dm new file mode 100644 index 0000000000..9f632a9242 --- /dev/null +++ b/code/modules/wiremod/components/math/logic.dm @@ -0,0 +1,57 @@ +#define COMP_LOGIC_AND "AND" +#define COMP_LOGIC_OR "OR" +#define COMP_LOGIC_XOR "XOR" + +/** + * # Logic Component + * + * General logic unit with AND OR capabilities + */ +/obj/item/circuit_component/compare/logic + display_name = "Logic" + desc = "A component with 'and' and 'or' capabilities." + + var/datum/port/input/option/logic_options + +/obj/item/circuit_component/compare/logic/populate_options() + var/static/component_options = list( + COMP_LOGIC_AND, + COMP_LOGIC_OR, + COMP_LOGIC_XOR, + ) + logic_options = add_option_port("Logic Options", component_options) + +/obj/item/circuit_component/compare/logic/do_comparisons(list/ports) + . = FALSE + var/current_option = logic_options.value + + // Used by XOR + var/total_ports = 0 + var/total_true_ports = 0 + for(var/datum/port/input/port as anything in ports) + if(isnull(port.value) && length(port.connected_ports) == 0) + continue + + total_ports += 1 + switch(current_option) + if(COMP_LOGIC_AND) + if(!port.value) + return FALSE + . = TRUE + if(COMP_LOGIC_OR) + if(port.value) + return TRUE + if(COMP_LOGIC_XOR) + if(port.value) + . = TRUE + total_true_ports += 1 + + if(current_option == COMP_LOGIC_XOR) + if(total_ports == total_true_ports) + return FALSE + if(.) + return TRUE + +#undef COMP_LOGIC_AND +#undef COMP_LOGIC_OR +#undef COMP_LOGIC_XOR diff --git a/code/modules/wiremod/components/math/not.dm b/code/modules/wiremod/components/math/not.dm new file mode 100644 index 0000000000..6a55024b54 --- /dev/null +++ b/code/modules/wiremod/components/math/not.dm @@ -0,0 +1,29 @@ +/** + * # Logic Component + * + * General logic unit with AND OR capabilities + */ +/obj/item/circuit_component/not + display_name = "Not" + desc = "A component that inverts its input." + + /// The input port + var/datum/port/input/input_port + + /// The result from the output + var/datum/port/output/result + circuit_flags = CIRCUIT_FLAG_INPUT_SIGNAL|CIRCUIT_FLAG_OUTPUT_SIGNAL + +/obj/item/circuit_component/not/Initialize() + . = ..() + input_port = add_input_port("Input", PORT_TYPE_ANY) + + result = add_output_port("Result", PORT_TYPE_NUMBER) + +/obj/item/circuit_component/not/input_received(datum/port/input/port) + . = ..() + if(.) + return + + result.set_output(!input_port.value) + diff --git a/code/modules/wiremod/components/math/random.dm b/code/modules/wiremod/components/math/random.dm new file mode 100644 index 0000000000..548376ed8d --- /dev/null +++ b/code/modules/wiremod/components/math/random.dm @@ -0,0 +1,40 @@ +/** + * # Random Component + * + * Generates a random number between specific values + */ +/obj/item/circuit_component/random + display_name = "Random" + desc = "A component that returns random values." + + /// The minimum value that the random number can be + var/datum/port/input/minimum + /// The maximum value that the random number can be + var/datum/port/input/maximum + + circuit_flags = CIRCUIT_FLAG_INPUT_SIGNAL|CIRCUIT_FLAG_OUTPUT_SIGNAL + + /// The result from the output + var/datum/port/output/output + +/obj/item/circuit_component/random/Initialize() + . = ..() + minimum = add_input_port("Minimum", PORT_TYPE_NUMBER, FALSE) + maximum = add_input_port("Maximum", PORT_TYPE_NUMBER, FALSE) + + output = add_output_port("Output", PORT_TYPE_NUMBER) + +/obj/item/circuit_component/random/input_received(datum/port/input/port) + . = ..() + if(.) + return + + var/min_val = minimum.value || 0 + var/max_val = maximum.value || 0 + + if(max_val < min_val) + output.set_output(0) + return + + output.set_output(rand(min_val, max_val)) + diff --git a/code/modules/wiremod/components/ntnet/ntnet_receive.dm b/code/modules/wiremod/components/ntnet/ntnet_receive.dm new file mode 100644 index 0000000000..0bcb1c5ed0 --- /dev/null +++ b/code/modules/wiremod/components/ntnet/ntnet_receive.dm @@ -0,0 +1,62 @@ +/** + * # NTNet Receiver Component + * + * Receives data through NTNet. + */ + +/obj/item/circuit_component/ntnet_receive + display_name = "NTNet Receiver" + desc = "Receives data packages through NTNet. If Encryption Key is set then only signals with the same Encryption Key will be received." + + circuit_flags = CIRCUIT_FLAG_OUTPUT_SIGNAL //trigger_output + + network_id = __NETWORK_CIRCUITS + + var/datum/port/input/push_hid + var/datum/port/output/hid + var/datum/port/output/data_package + var/datum/port/output/secondary_package + var/datum/port/input/enc_key + var/datum/port/input/option/data_type_options + var/datum/port/input/option/secondary_data_type_options + +/obj/item/circuit_component/ntnet_receive/Initialize() + . = ..() + data_package = add_output_port("Data Package", PORT_TYPE_ANY) + secondary_package = add_output_port("Secondary Package", PORT_TYPE_ANY) + enc_key = add_input_port("Encryption Key", PORT_TYPE_STRING) + RegisterSignal(src, COMSIG_COMPONENT_NTNET_RECEIVE, .proc/ntnet_receive) + +/obj/item/circuit_component/ntnet_receive/populate_options() + var/static/component_options = list( + PORT_TYPE_ANY, + PORT_TYPE_STRING, + PORT_TYPE_NUMBER, + PORT_TYPE_LIST, + PORT_TYPE_ATOM, + ) + data_type_options = add_option_port("Data Type", component_options) + secondary_data_type_options = add_option_port("Secondary Data Type", component_options) + +/obj/item/circuit_component/ntnet_receive/input_received(datum/port/input/port) + . = ..() + if(.) + return + + if(COMPONENT_TRIGGERED_BY(data_type_options, port)) + data_package.set_datatype(data_type_options.value) + + if(COMPONENT_TRIGGERED_BY(secondary_data_type_options, port)) + secondary_package.set_datatype(secondary_data_type_options.value) + + return TRUE + +/obj/item/circuit_component/ntnet_receive/proc/ntnet_receive(datum/source, datum/netdata/data) + SIGNAL_HANDLER + + if(data.data["enc_key"] != enc_key.value) + return + + data_package.set_output(data.data["data"]) + secondary_package.set_output(data.data["data_secondary"]) + trigger_output.set_output(COMPONENT_SIGNAL) diff --git a/code/modules/wiremod/components/ntnet/ntnet_send.dm b/code/modules/wiremod/components/ntnet/ntnet_send.dm new file mode 100644 index 0000000000..33d9370fae --- /dev/null +++ b/code/modules/wiremod/components/ntnet/ntnet_send.dm @@ -0,0 +1,29 @@ +/** + * # NTNet Transmitter Component + * + * Sends a data package through NTNet + */ + +/obj/item/circuit_component/ntnet_send + display_name = "NTNet Transmitter" + desc = "Sends a data package through NTNet. If Encryption Key is set then transmitted data will be only picked up by receivers with the same Encryption Key." + + circuit_flags = CIRCUIT_FLAG_INPUT_SIGNAL + + network_id = __NETWORK_CIRCUITS + + var/datum/port/input/data_package + var/datum/port/input/secondary_package + var/datum/port/input/enc_key + +/obj/item/circuit_component/ntnet_send/Initialize() + . = ..() + data_package = add_input_port("Data Package", PORT_TYPE_ANY) + secondary_package = add_input_port("Secondary Package", PORT_TYPE_ANY) + enc_key = add_input_port("Encryption Key", PORT_TYPE_STRING) + +/obj/item/circuit_component/ntnet_send/input_received(datum/port/input/port) + . = ..() + if(.) + return + ntnet_send(list("data" = data_package.value, "data_secondary" = secondary_package.value, "enc_key" = enc_key.value)) diff --git a/code/modules/wiremod/components/sensors/pressuresensor.dm b/code/modules/wiremod/components/sensors/pressuresensor.dm new file mode 100644 index 0000000000..25f39b9c8e --- /dev/null +++ b/code/modules/wiremod/components/sensors/pressuresensor.dm @@ -0,0 +1,36 @@ +/** + * # Pressure Sensor + * + * Returns the pressure of the tile + */ +/obj/item/circuit_component/pressuresensor + display_name = "Pressure Sensor" + desc = "Outputs the current pressure of the tile" + + circuit_flags = CIRCUIT_FLAG_INPUT_SIGNAL|CIRCUIT_FLAG_OUTPUT_SIGNAL + + /// The result from the output + var/datum/port/output/result + +/obj/item/circuit_component/pressuresensor/Initialize() + . = ..() + result = add_output_port("Result", PORT_TYPE_NUMBER) + +/obj/item/circuit_component/pressuresensor/input_received(datum/port/input/port) + . = ..() + if(.) + return + //Get current turf + var/turf/location = get_turf(src) + if(!location) + result.set_output(null) + return + //Get environment info + var/datum/gas_mixture/environment = location.return_air() + var/total_moles = environment.total_moles() + var/pressure = environment.return_pressure() + if(total_moles) + //If there's atmos, return pressure + result.set_output(round(pressure,1)) + else + result.set_output(0) diff --git a/code/modules/wiremod/components/sensors/tempsensor.dm b/code/modules/wiremod/components/sensors/tempsensor.dm new file mode 100644 index 0000000000..bb2410ce77 --- /dev/null +++ b/code/modules/wiremod/components/sensors/tempsensor.dm @@ -0,0 +1,36 @@ +/** + * # Temperature Sensor + * + * Returns the temperature of the tile + */ +/obj/item/circuit_component/tempsensor + display_name = "Temperature Sensor" + desc = "Outputs the current temperature of the tile" + + circuit_flags = CIRCUIT_FLAG_INPUT_SIGNAL|CIRCUIT_FLAG_OUTPUT_SIGNAL + + /// The result from the output + var/datum/port/output/result + +/obj/item/circuit_component/tempsensor/Initialize() + . = ..() + result = add_output_port("Result", PORT_TYPE_NUMBER) + +/obj/item/circuit_component/tempsensor/input_received(datum/port/input/port) + . = ..() + if(.) + return + //Get current turf + var/turf/location = get_turf(src) + if(!location) + result.set_output(null) + return + //Get environment info + var/datum/gas_mixture/environment = location.return_air() + var/total_moles = environment.total_moles() + if(total_moles) + //If there's atmos, return temperature + result.set_output(round(environment.temperature,1)) + else + result.set_output(0) + diff --git a/code/modules/wiremod/components/string/concat.dm b/code/modules/wiremod/components/string/concat.dm new file mode 100644 index 0000000000..55dd5530a1 --- /dev/null +++ b/code/modules/wiremod/components/string/concat.dm @@ -0,0 +1,42 @@ +/** + * # Concatenate Component + * + * General string concatenation component. Puts strings together. + */ +/obj/item/circuit_component/concat + display_name = "Concatenate" + desc = "A component that combines strings." + + /// The amount of input ports to have + var/input_port_amount = 4 + + /// The result from the output + var/datum/port/output/output + circuit_flags = CIRCUIT_FLAG_INPUT_SIGNAL|CIRCUIT_FLAG_OUTPUT_SIGNAL + +/obj/item/circuit_component/concat/Initialize() + . = ..() + for(var/port_id in 1 to input_port_amount) + var/letter = ascii2text(text2ascii("A") + (port_id-1)) + add_input_port(letter, PORT_TYPE_STRING) + + output = add_output_port("Output", PORT_TYPE_STRING) + +/obj/item/circuit_component/concat/input_received(datum/port/input/port) + . = ..() + if(.) + return + + var/result = "" + var/list/ports = input_ports.Copy() + ports -= trigger_input + + for(var/datum/port/input/input_port as anything in ports) + var/value = input_port.value + if(isnull(value)) + continue + + result += "[value]" + + output.set_output(result) + diff --git a/code/modules/wiremod/components/string/contains.dm b/code/modules/wiremod/components/string/contains.dm new file mode 100644 index 0000000000..61b5e0ca22 --- /dev/null +++ b/code/modules/wiremod/components/string/contains.dm @@ -0,0 +1,35 @@ +/** + * # String Contains Component + * + * Checks if a string contains a word/letter + */ +/obj/item/circuit_component/compare/contains + display_name = "String Contains" + desc = "Checks if a string contains a word/letter" + + input_port_amount = 0 + + var/datum/port/input/needle + var/datum/port/input/haystack + +/obj/item/circuit_component/compare/contains/load_custom_ports() + needle = add_input_port("Needle", PORT_TYPE_STRING) + haystack = add_input_port("Haystack", PORT_TYPE_STRING) + +/obj/item/circuit_component/compare/contains/Destroy() + needle = null + haystack = null + return ..() + + +/obj/item/circuit_component/compare/contains/do_comparisons(list/ports) + if(length(ports) < input_port_amount) + return + + var/to_find = needle.value + var/to_search = haystack.value + + if(!to_find || !to_search) + return + + return findtext(to_search, to_find) diff --git a/code/modules/wiremod/components/string/textcase.dm b/code/modules/wiremod/components/string/textcase.dm new file mode 100644 index 0000000000..a021074433 --- /dev/null +++ b/code/modules/wiremod/components/string/textcase.dm @@ -0,0 +1,54 @@ +#define COMP_TEXT_LOWER "To Lower" +#define COMP_TEXT_UPPER "To Upper" + +/** + * # Text Component + * + * Either makes the text upper case or lower case. + */ +/obj/item/circuit_component/textcase + display_name = "Text Case" + desc = "A component that makes its input uppercase or lowercase." + + var/datum/port/input/option/textcase_options + + /// The input port + var/datum/port/input/input_port + + /// The result of the text operation + var/datum/port/output/output + + circuit_flags = CIRCUIT_FLAG_INPUT_SIGNAL|CIRCUIT_FLAG_OUTPUT_SIGNAL + +/obj/item/circuit_component/textcase/populate_options() + var/static/component_options = list( + COMP_TEXT_LOWER, + COMP_TEXT_UPPER, + ) + textcase_options = add_option_port("Textcase Options", component_options) + +/obj/item/circuit_component/textcase/Initialize() + . = ..() + input_port = add_input_port("Input", PORT_TYPE_STRING) + output = add_output_port("Output", PORT_TYPE_STRING) + +/obj/item/circuit_component/textcase/input_received(datum/port/input/port) + . = ..() + if(.) + return + + var/value = input_port.value + if(isnull(value)) + return + + var/result + switch(textcase_options.value) + if(COMP_TEXT_LOWER) + result = lowertext(value) + if(COMP_TEXT_UPPER) + result = uppertext(value) + + output.set_output(result) + +#undef COMP_TEXT_LOWER +#undef COMP_TEXT_UPPER diff --git a/code/modules/wiremod/components/string/tonumber.dm b/code/modules/wiremod/components/string/tonumber.dm new file mode 100644 index 0000000000..6012ab339d --- /dev/null +++ b/code/modules/wiremod/components/string/tonumber.dm @@ -0,0 +1,29 @@ +/** + * #To Number Component + * + * Converts a string into a Number + */ +/obj/item/circuit_component/tonumber + display_name = "To Number" + desc = "A component that converts its input (a string) to a number. If there's text in the input, it'll only consider it if it starts with a number. It will take that number and ignore the rest." + + /// The input port + var/datum/port/input/input_port + + /// The result from the output + var/datum/port/output/output + + circuit_flags = CIRCUIT_FLAG_INPUT_SIGNAL|CIRCUIT_FLAG_OUTPUT_SIGNAL + +/obj/item/circuit_component/tonumber/Initialize() + . = ..() + input_port = add_input_port("Input", PORT_TYPE_STRING) + + output = add_output_port("Output", PORT_TYPE_NUMBER) + +/obj/item/circuit_component/tonumber/input_received(datum/port/input/port) + . = ..() + if(.) + return + + output.set_output(text2num(input_port.value)) diff --git a/code/modules/wiremod/components/string/tostring.dm b/code/modules/wiremod/components/string/tostring.dm new file mode 100644 index 0000000000..bc044f157a --- /dev/null +++ b/code/modules/wiremod/components/string/tostring.dm @@ -0,0 +1,40 @@ +/** + * # To String Component + * + * Converts any value into a string + */ +/obj/item/circuit_component/tostring + display_name = "To String" + desc = "A component that converts its input to text." + + /// The input port + var/datum/port/input/input_port + + /// The result from the output + var/datum/port/output/output + + circuit_flags = CIRCUIT_FLAG_INPUT_SIGNAL|CIRCUIT_FLAG_OUTPUT_SIGNAL + + var/max_range = 5 + +/obj/item/circuit_component/tostring/Initialize() + . = ..() + input_port = add_input_port("Input", PORT_TYPE_ANY) + + output = add_output_port("Output", PORT_TYPE_STRING) + +/obj/item/circuit_component/tostring/input_received(datum/port/input/port) + . = ..() + if(.) + return + + var/value = input_port.value + if(isatom(value)) + var/turf/location = get_turf(src) + var/atom/object = value + if(object.z != location.z || get_dist(location, object) > max_range) + output.set_output(PORT_TYPE_ATOM) + return + + output.set_output("[value]") + diff --git a/code/modules/wiremod/components/utility/clock.dm b/code/modules/wiremod/components/utility/clock.dm new file mode 100644 index 0000000000..4c570b0ec5 --- /dev/null +++ b/code/modules/wiremod/components/utility/clock.dm @@ -0,0 +1,57 @@ +/** + * # Clock Component + * + * Fires every tick of the circuit timer SS + */ +/obj/item/circuit_component/clock + display_name = "Clock" + desc = "A component that repeatedly fires." + + /// Whether the clock is on or not + var/datum/port/input/on + + /// The signal from this clock component + var/datum/port/output/signal + +/obj/item/circuit_component/clock/get_ui_notices() + . = ..() + . += create_ui_notice("Clock Interval: [DisplayTimeText(COMP_CLOCK_DELAY)]", "orange", "clock") + +/obj/item/circuit_component/clock/Initialize() + . = ..() + on = add_input_port("On", PORT_TYPE_NUMBER) + + signal = add_output_port("Signal", PORT_TYPE_SIGNAL) + +/obj/item/circuit_component/clock/input_received(datum/port/input/port) + . = ..() + if(.) + return + + if(on.value) + start_process() + else + stop_process() + +/obj/item/circuit_component/clock/Destroy() + stop_process() + return ..() + +/obj/item/circuit_component/clock/process(delta_time) + signal.set_output(COMPONENT_SIGNAL) + +/** + * Adds the component to the SSclock_component process list + * + * Starts ticking to send signals between periods of time + */ +/obj/item/circuit_component/clock/proc/start_process() + START_PROCESSING(SSclock_component, src) + +/** + * Removes the component to the SSclock_component process list + * + * Signals stop getting sent. + */ +/obj/item/circuit_component/clock/proc/stop_process() + STOP_PROCESSING(SSclock_component, src) diff --git a/code/modules/wiremod/components/utility/delay.dm b/code/modules/wiremod/components/utility/delay.dm new file mode 100644 index 0000000000..37fa5a79f6 --- /dev/null +++ b/code/modules/wiremod/components/utility/delay.dm @@ -0,0 +1,43 @@ +/// The minimum delay value that the delay component can have. +#define COMP_DELAY_MIN_VALUE 0.1 + +/** + * # Delay Component + * + * Delays a signal by a specified duration. + */ +/obj/item/circuit_component/delay + display_name = "Delay" + desc = "A component that delays a signal by a specified duration." + + /// Amount to delay by + var/datum/port/input/delay_amount + /// Input signal to fire the delay + var/datum/port/input/trigger + + /// The output of the signal + var/datum/port/output/output + +/obj/item/circuit_component/delay/Initialize() + . = ..() + delay_amount = add_input_port("Delay", PORT_TYPE_NUMBER, FALSE) + trigger = add_input_port("Trigger", PORT_TYPE_SIGNAL) + + output = add_output_port("Result", PORT_TYPE_SIGNAL) + +/obj/item/circuit_component/delay/input_received(datum/port/input/port) + . = ..() + if(.) + return + + if(!COMPONENT_TRIGGERED_BY(trigger, port)) + return + + var/delay = delay_amount.value + if(delay > COMP_DELAY_MIN_VALUE) + // Convert delay into deciseconds + addtimer(CALLBACK(output, /datum/port/output.proc/set_output, trigger.value), delay*10) + else + output.set_output(trigger.value) + +#undef COMP_DELAY_MIN_VALUE diff --git a/code/modules/wiremod/components/utility/getter.dm b/code/modules/wiremod/components/utility/getter.dm new file mode 100644 index 0000000000..7bca3083da --- /dev/null +++ b/code/modules/wiremod/components/utility/getter.dm @@ -0,0 +1,70 @@ +/** + * # Getter Component + * + * Gets the current value from a variable. + */ +/obj/item/circuit_component/getter + display_name = "Variable Getter" + desc = "A component that gets a variable globally on the circuit." + + /// Variable name + var/datum/port/input/option/variable_name + + /// The value of the variable + var/datum/port/output/value + + var/datum/circuit_variable/current_variable + +/obj/item/circuit_component/getter/populate_options() + variable_name = add_option_port("Variable", null) + +/obj/item/circuit_component/getter/add_to(obj/item/integrated_circuit/added_to) + . = ..() + variable_name.possible_options = added_to.circuit_variables + +/obj/item/circuit_component/getter/removed_from(obj/item/integrated_circuit/removed_from) + variable_name.possible_options = null + return ..() + +/obj/item/circuit_component/getter/Initialize() + . = ..() + value = add_output_port("Value", PORT_TYPE_ANY) + +/obj/item/circuit_component/getter/input_received(datum/port/input/port) + . = ..() + // We don't care much about the parent's return value. We only care if the parent exists + // since this should never really fail. + if(!parent) + return + + var/variable_string = variable_name.value + if(!variable_string) + remove_current_variable() + value.set_output(null) + return + + var/datum/circuit_variable/variable = parent.circuit_variables[variable_string] + if(!variable) + remove_current_variable() + value.set_output(null) + return + + set_current_variable(variable) + value.set_output(variable.value) + +/obj/item/circuit_component/getter/proc/remove_current_variable() + SIGNAL_HANDLER + if(current_variable) + current_variable.remove_listener(src) + UnregisterSignal(current_variable, COMSIG_PARENT_QDELETING) + current_variable = null + +/obj/item/circuit_component/getter/proc/set_current_variable(datum/circuit_variable/variable) + if(variable == current_variable) + return + + remove_current_variable() + current_variable = variable + current_variable.add_listener(src) + RegisterSignal(current_variable, COMSIG_PARENT_QDELETING, .proc/remove_current_variable) + value.set_datatype(variable.datatype) diff --git a/code/modules/wiremod/components/utility/router.dm b/code/modules/wiremod/components/utility/router.dm new file mode 100644 index 0000000000..4086392f5b --- /dev/null +++ b/code/modules/wiremod/components/utility/router.dm @@ -0,0 +1,82 @@ +/** + * # Router Component + * + * Writes one of multiple inputs to one of multiple outputs. + */ +/obj/item/circuit_component/router + display_name = "Router" + desc = "Copies the input chosen by \"Input Selector\" to the output chosen by \"Output Selector\"." + circuit_flags = CIRCUIT_FLAG_INPUT_SIGNAL|CIRCUIT_FLAG_OUTPUT_SIGNAL + + var/datum/port/input/option/router_options + + /// Which ports to connect. + var/datum/port/input/input_selector + var/datum/port/input/output_selector + + /// How many ports to have. + var/input_port_amount = 4 + var/output_port_amount = 4 + + /// Current type of the ports + var/current_type + + /// The ports to route. + var/list/datum/port/input/ins + var/list/datum/port/output/outs + +/obj/item/circuit_component/router/populate_options() + var/static/component_options = list( + PORT_TYPE_ANY, + PORT_TYPE_STRING, + PORT_TYPE_NUMBER, + PORT_TYPE_LIST, + PORT_TYPE_ATOM, + ) + router_options = add_option_port("Router Options", component_options) + +/obj/item/circuit_component/router/Initialize() + . = ..() + current_type = router_options.value + if(input_port_amount > 1) + input_selector = add_input_port("Input Selector", PORT_TYPE_NUMBER, default = 1) + if(output_port_amount > 1) + output_selector = add_input_port("Output Selector", PORT_TYPE_NUMBER, default = 1) + ins = list() + for(var/port_id in 1 to input_port_amount) + ins += add_input_port(input_port_amount > 1 ? "Input [port_id]" : "Input", current_type) + outs = list() + for(var/port_id in 1 to output_port_amount) + outs += add_output_port(output_port_amount > 1 ? "Output [port_id]" : "Output", current_type) + +/obj/item/circuit_component/router/Destroy() + input_selector = null + output_selector = null + ins.Cut() + ins = null + outs.Cut() + outs = null + return ..() + + +// If I is in range, L[I]. If I is out of range, wrap around. +#define WRAPACCESS(L, I) L[(((I||1)-1)%length(L)+length(L))%length(L)+1] +/obj/item/circuit_component/router/input_received(datum/port/input/port) + . = ..() + var/current_option = router_options.value + if(current_type != current_option) + current_type = current_option + for(var/datum/port/input/input as anything in ins) + input.set_datatype(current_type) + for(var/datum/port/output/output as anything in outs) + output.set_datatype(current_type) + if(.) + return + var/datum/port/input/input = WRAPACCESS(ins, input_selector ? input_selector.value : 1) + var/datum/port/output/output = WRAPACCESS(outs, output_selector ? output_selector.value : 1) + output.set_output(input.value) + +/obj/item/circuit_component/router/multiplexer + display_name = "Multiplexer" + desc = "Copies the input chosen by \"Input Selector\" to the output." + output_port_amount = 1 diff --git a/code/modules/wiremod/components/utility/setter.dm b/code/modules/wiremod/components/utility/setter.dm new file mode 100644 index 0000000000..614e700798 --- /dev/null +++ b/code/modules/wiremod/components/utility/setter.dm @@ -0,0 +1,58 @@ +/** + * # Setter Component + * + * Stores the current input when triggered into a variable. + */ +/obj/item/circuit_component/setter + display_name = "Variable Setter" + desc = "A component that sets a variable globally on the circuit." + + circuit_flags = CIRCUIT_FLAG_OUTPUT_SIGNAL + + /// Variable name + var/datum/port/input/option/variable_name + + /// The input to store + var/datum/port/input/input_port + /// The trigger to store the current value of the input + var/datum/port/input/trigger + + var/current_type + +/obj/item/circuit_component/setter/populate_options() + variable_name = add_option_port("Variable", null) + +/obj/item/circuit_component/setter/add_to(obj/item/integrated_circuit/added_to) + . = ..() + variable_name.possible_options = added_to.circuit_variables + +/obj/item/circuit_component/setter/removed_from(obj/item/integrated_circuit/removed_from) + variable_name.possible_options = null + return ..() + +/obj/item/circuit_component/setter/Initialize() + . = ..() + input_port = add_input_port("Input", PORT_TYPE_ANY) + trigger = add_input_port("Store", PORT_TYPE_SIGNAL) + +/obj/item/circuit_component/setter/input_received(datum/port/input/port) + . = ..() + var/variable_string = variable_name.value + if(!variable_string) + return + + var/datum/circuit_variable/variable = parent.circuit_variables[variable_string] + if(!variable) + return + + if(variable.datatype != current_type) + current_type = variable.datatype + input_port.set_datatype(current_type) + + if(.) + return + + if(!COMPONENT_TRIGGERED_BY(trigger, port)) + return TRUE + + variable.set_value(input_port.value) diff --git a/code/modules/wiremod/components/utility/typecast.dm b/code/modules/wiremod/components/utility/typecast.dm new file mode 100644 index 0000000000..ee204c332e --- /dev/null +++ b/code/modules/wiremod/components/utility/typecast.dm @@ -0,0 +1,60 @@ +/** + * # Typecast Component + * + * A component that casts a value to a type if it matches or outputs null. + */ +/obj/item/circuit_component/typecast + display_name = "Typecast" + desc = "A component that casts a value to a type if it matches or outputs null." + circuit_flags = CIRCUIT_FLAG_INPUT_SIGNAL|CIRCUIT_FLAG_OUTPUT_SIGNAL + + var/datum/port/input/option/typecast_options + + var/datum/port/input/input_value + + var/datum/port/output/output_value + + var/current_type + +/obj/item/circuit_component/typecast/Initialize() + . = ..() + current_type = typecast_options.value + input_value = add_input_port("Input", PORT_TYPE_ANY) + output_value = add_output_port("Output", current_type) + +/obj/item/circuit_component/typecast/populate_options() + var/static/list/component_options = list( + PORT_TYPE_STRING, + PORT_TYPE_NUMBER, + PORT_TYPE_LIST, + PORT_TYPE_ATOM, + ) + typecast_options = add_option_port("Typecast Options", component_options) + +/obj/item/circuit_component/typecast/input_received(datum/port/input/port) + . = ..() + var/current_option = typecast_options.value + if(current_type != current_option) + current_type = current_option + output_value.set_datatype(current_type) + + if(.) + return + + var/value = input_value.value + var/value_to_set = null + switch(current_option) + if(PORT_TYPE_STRING) + if(istext(value)) + value_to_set = value + if(PORT_TYPE_NUMBER) + if(isnum(value)) + value_to_set = value + if(PORT_TYPE_LIST) + if(islist(value)) + value_to_set = value + if(PORT_TYPE_ATOM) + if(isatom(value)) + value_to_set = value + + output_value.set_output(value_to_set) diff --git a/code/modules/wiremod/components/utility/typecheck.dm b/code/modules/wiremod/components/utility/typecheck.dm new file mode 100644 index 0000000000..8caa009fd8 --- /dev/null +++ b/code/modules/wiremod/components/utility/typecheck.dm @@ -0,0 +1,51 @@ +#define COMP_TYPECHECK_MOB "organism" +#define COMP_TYPECHECK_HUMAN "humanoid" + +/** + * # Typecheck Component + * + * Checks the type of a value + */ +/obj/item/circuit_component/compare/typecheck + display_name = "Typecheck" + desc = "A component that checks the type of its input." + + input_port_amount = 1 + var/datum/port/input/option/typecheck_options + +/obj/item/circuit_component/compare/typecheck/populate_options() + var/static/component_options = list( + PORT_TYPE_STRING, + PORT_TYPE_NUMBER, + PORT_TYPE_LIST, + PORT_TYPE_ATOM, + COMP_TYPECHECK_MOB, + COMP_TYPECHECK_HUMAN, + ) + typecheck_options = add_option_port("Typecheck Options", component_options) + +/obj/item/circuit_component/compare/typecheck/do_comparisons(list/ports) + if(!length(ports)) + return + . = FALSE + + // We're only comparing the first port/value. There shouldn't be any more. + var/datum/port/input/input_port = ports[1] + var/input_val = input_port.value + switch(typecheck_options.value) + if(PORT_TYPE_STRING) + return istext(input_val) + if(PORT_TYPE_NUMBER) + return isnum(input_val) + if(PORT_TYPE_LIST) + return islist(input_val) + if(PORT_TYPE_ATOM) + return isatom(input_val) + if(COMP_TYPECHECK_MOB) + return ismob(input_val) + if(COMP_TYPECHECK_HUMAN) + return ishuman(input_val) + + +#undef COMP_TYPECHECK_MOB +#undef COMP_TYPECHECK_HUMAN diff --git a/code/modules/wiremod/core/admin_panel.dm b/code/modules/wiremod/core/admin_panel.dm new file mode 100644 index 0000000000..33af02f514 --- /dev/null +++ b/code/modules/wiremod/core/admin_panel.dm @@ -0,0 +1,75 @@ +/// An admin verb to view all circuits, plus useful information +/datum/admins/proc/view_all_circuits() + set category = "Admin.Game" + set name = "View All Circuits" + + var/static/datum/circuit_admin_panel/circuit_admin_panel = new + circuit_admin_panel.ui_interact(usr) + +/datum/circuit_admin_panel + +/datum/circuit_admin_panel/ui_static_data(mob/user) + var/list/data = list() + data["circuits"] = list() + + for (var/obj/item/integrated_circuit/circuit as anything in GLOB.integrated_circuits) + var/datum/mind/inserter = circuit.inserter_mind?.resolve() + + data["circuits"] += list(list( + "ref" = REF(circuit), + "name" = "[circuit.name] in [loc_name(circuit)]", + "creator" = circuit.get_creator(), + "has_inserter" = !isnull(inserter), + )) + + return data + +/datum/circuit_admin_panel/ui_act(action, list/params, datum/tgui/ui, datum/ui_state/state) + . = ..() + if (.) + return . + + if (!istext(params["circuit"])) + return FALSE + + var/obj/item/integrated_circuit/circuit = locate(params["circuit"]) + if (!istype(circuit)) + to_chat(usr, span_warning("That circuit no longer exists.")) + return FALSE + + switch (action) + if ("duplicate_circuit") + if (alert(usr, "This will spawn the new circuit at where you are, are you sure?", "Confirm", "Yes", "No") != "Yes") + return FALSE + + var/list/errors = list() + + var/obj/item/integrated_circuit/loaded/new_circuit = new(usr.drop_location()) + new_circuit.load_circuit_data(circuit.convert_to_json(), errors) + + if (length(errors)) + to_chat(usr, span_warning("Somehow, duplicating the circuit failed:")) + for (var/error in errors) + to_chat(usr, span_warning(error)) + if ("follow_circuit") + usr.client?.admin_follow(circuit) + if ("save_circuit") + circuit.attempt_save_to(usr.client) + if ("vv_circuit") + usr.client?.debug_variables(circuit) + if ("open_circuit") + circuit.ui_interact(usr) + if ("open_player_panel") + var/datum/mind/inserter = circuit.inserter_mind?.resolve() + usr.client?.holder?.show_player_panel(inserter?.current) + + return TRUE + +/datum/circuit_admin_panel/ui_state(mob/user) + return GLOB.admin_state + +/datum/circuit_admin_panel/ui_interact(mob/user, datum/tgui/ui) + ui = SStgui.try_update_ui(user, src, ui) + if(!ui) + ui = new(user, src, "CircuitAdminPanel") + ui.open() diff --git a/code/modules/wiremod/core/component.dm b/code/modules/wiremod/core/component.dm new file mode 100644 index 0000000000..0939debc3c --- /dev/null +++ b/code/modules/wiremod/core/component.dm @@ -0,0 +1,270 @@ +/** + * # Integrated Circuit Component + * + * A component that performs a function when given an input + * + * Can be attached to an integrated circuitboard, where it can then + * be connected between other components to provide an output or to receive + * an input. This is the base type of all components + */ +/obj/item/circuit_component + name = COMPONENT_DEFAULT_NAME + icon = 'icons/obj/module.dmi' + icon_state = "component" + inhand_icon_state = "electronic" + + /// The name of the component shown on the UI + var/display_name = "Generic" + + /// The integrated_circuit that this component is attached to. + var/obj/item/integrated_circuit/parent + + /// A list that contains the outpurt ports on this component + /// Used to connect between the ports + var/list/datum/port/output/output_ports = list() + + /// A list that contains the components the input ports on this component + /// Used to connect between the ports + var/list/datum/port/input/input_ports = list() + + /// Generic trigger input for triggering this component + var/datum/port/input/trigger_input + var/datum/port/output/trigger_output + + /// The flags of the circuit to control basic generalised behaviour. + var/circuit_flags = NONE + + /// Used to determine the x position of the component within the UI + var/rel_x = 0 + /// Used to determine the y position of the component within the UI + var/rel_y = 0 + + /// The power usage whenever this component receives an input + var/power_usage_per_input = 1 + + // Whether the component is removable or not. Only affects user UI + var/removable = TRUE + + // Defines which shells support this component. Only used as an informational guide, does not restrict placing these components in circuits. + var/required_shells = null + +/// Called when the option ports should be set up +/obj/item/circuit_component/proc/populate_options() + return + +/// Extension of add_input_port. Simplifies the code to make an option port to reduce boilerplate +/obj/item/circuit_component/proc/add_option_port(name, list/list_to_use) + return add_input_port(name, PORT_TYPE_OPTION, port_type = /datum/port/input/option, extra_args = list("possible_options" = list_to_use)) + +/obj/item/circuit_component/Initialize() + . = ..() + if(name == COMPONENT_DEFAULT_NAME) + name = "[lowertext(display_name)] [COMPONENT_DEFAULT_NAME]" + populate_options() + + return INITIALIZE_HINT_LATELOAD + +/obj/item/circuit_component/LateInitialize() + . = ..() + if(circuit_flags & CIRCUIT_FLAG_INPUT_SIGNAL) + trigger_input = add_input_port("Trigger", PORT_TYPE_SIGNAL) + if(circuit_flags & CIRCUIT_FLAG_OUTPUT_SIGNAL) + trigger_output = add_output_port("Triggered", PORT_TYPE_SIGNAL) + +/obj/item/circuit_component/Destroy() + if(parent) + // Prevents a Destroy() recursion + var/obj/item/integrated_circuit/old_parent = parent + parent = null + old_parent.remove_component(src) + + trigger_input = null + trigger_output = null + + QDEL_LIST(output_ports) + QDEL_LIST(input_ports) + return ..() + +/** + * Called when a shell is registered from the component/the component is added to a circuit. + * + * Register all signals here on the shell. + * Arguments: + * * shell - Shell being registered + */ +/obj/item/circuit_component/proc/register_shell(atom/movable/shell) + return + +/** + * Called when a shell is unregistered from the component/the component is removed from a circuit. + * + * Unregister all signals here on the shell. + * Arguments: + * * shell - Shell being unregistered + */ +/obj/item/circuit_component/proc/unregister_shell(atom/movable/shell) + return + +/** + * Disconnects a component from other components + * + * Disconnects both the input and output ports of the component + */ +/obj/item/circuit_component/proc/disconnect() + for(var/datum/port/output/port_to_disconnect as anything in output_ports) + port_to_disconnect.disconnect_all() + + for(var/datum/port/input/port_to_disconnect as anything in input_ports) + port_to_disconnect.disconnect_all() + +/** + * Adds an input port and returns it + * + * Arguments: + * * name - The name of the input port + * * type - The datatype it handles + * * trigger - Whether this input port triggers an update on the component when updated. + */ +/obj/item/circuit_component/proc/add_input_port(name, type, trigger = TRUE, default = null, index = null, port_type = /datum/port/input, extra_args = null) + var/list/arguments = list(src) + arguments += args + if(extra_args) + arguments += extra_args + var/datum/port/input/input_port = new port_type(arglist(arguments)) + if(index) + input_ports.Insert(index, input_port) + else + input_ports += input_port + if(parent) + SStgui.update_uis(parent) + return input_port + +/** + * Removes an input port and deletes it. This will not cleanup any references made by derivatives of the circuit component + * + * Arguments: + * * input_port - The input port to remove. + */ +/obj/item/circuit_component/proc/remove_input_port(datum/port/input/input_port) + input_ports -= input_port + qdel(input_port) + if(parent) + SStgui.update_uis(parent) + +/** + * Adds an output port and returns it + * + * Arguments: + * * name - The name of the output port + * * type - The datatype it handles. + */ +/obj/item/circuit_component/proc/add_output_port(name, type) + var/list/arguments = list(src) + arguments += args + var/datum/port/output/output_port = new(arglist(arguments)) + output_ports += output_port + return output_port + +/** + * Removes an output port and deletes it. This will not cleanup any references made by derivatives of the circuit component + * + * Arguments: + * * output_port - The output port to remove. + */ +/obj/item/circuit_component/proc/remove_output_port(datum/port/output/output_port) + output_ports -= output_port + qdel(output_port) + if(parent) + SStgui.update_uis(parent) + +/** + * Called whenever an input is received from one of the ports. + * + * Return value indicates that the circuit should not do anything. Also prevents an output signal. + * Arguments: + * * port - Can be null. The port that sent the input + */ +/obj/item/circuit_component/proc/input_received(datum/port/input/port) + SHOULD_CALL_PARENT(TRUE) + if(!parent?.on) + return TRUE + + if(!parent.admin_only) + if(circuit_flags & CIRCUIT_FLAG_ADMIN) + message_admins("[display_name] tried to execute on [parent.get_creator_admin()] that has admin_only set to 0") + return TRUE + + var/obj/item/stock_parts/cell/cell = parent.get_cell() + if(!cell?.use(power_usage_per_input)) + return TRUE + + if((circuit_flags & CIRCUIT_FLAG_INPUT_SIGNAL) && !COMPONENT_TRIGGERED_BY(trigger_input, port)) + return TRUE + +/// Called when this component is about to be added to an integrated_circuit. +/obj/item/circuit_component/proc/add_to(obj/item/integrated_circuit/added_to) + return TRUE + +/// Called when this component is removed from an integrated_circuit. +/obj/item/circuit_component/proc/removed_from(obj/item/integrated_circuit/removed_from) + return + +/** + * Gets the UI notices to be displayed on the CircuitInfo panel. + * + * Returns a list of buttons in the following format + * list( + * "icon" = ICON(string) + * "content" = CONTENT(string) + * "color" = COLOR(string, not a hex) + * ) + */ +/obj/item/circuit_component/proc/get_ui_notices() + . = list() + + if(!removable) + . += create_ui_notice("Unremovable", "red", "lock") + + if(length(required_shells)) + . += create_ui_notice("Supported Shells:", "green", "notes-medical") + for(var/atom/movable/shell as anything in required_shells) + . += create_ui_notice(initial(shell.name), "green", "plus-square") + + if(length(input_ports)) + . += create_ui_notice("Power Usage Per Input: [power_usage_per_input]", "orange", "bolt") + +/** + * Creates a UI notice entry to be used in get_ui_notices() + * + * Returns a list that can then be added to the return list in get_ui_notices() + */ +/obj/item/circuit_component/proc/create_ui_notice(content, color, icon) + SHOULD_BE_PURE(TRUE) + SHOULD_NOT_OVERRIDE(TRUE) + return list(list( + "icon" = icon, + "content" = content, + "color" = color, + )) + +/** + * Creates a table UI notice entry to be used in get_ui_notices() + * + * Returns a list that can then be added to the return list in get_ui_notices() + * Used by components to list their available columns. Recommended to use at the end of get_ui_notices() + */ +/obj/item/circuit_component/proc/create_table_notices(list/entries) + SHOULD_BE_PURE(TRUE) + SHOULD_NOT_OVERRIDE(TRUE) + . = list() + . += create_ui_notice("Available Columns:", "grey", "question-circle") + + + for(var/entry in entries) + . += create_ui_notice("Column Name: '[entry]'", "grey", "columns") + +/obj/item/circuit_component/proc/register_usb_parent(atom/movable/parent) + return + +/obj/item/circuit_component/proc/unregister_usb_parent(atom/movable/parent) + return diff --git a/code/modules/wiremod/core/component_printer.dm b/code/modules/wiremod/core/component_printer.dm new file mode 100644 index 0000000000..7400aafbf2 --- /dev/null +++ b/code/modules/wiremod/core/component_printer.dm @@ -0,0 +1,394 @@ +/// Component printer, creates components for integrated circuits. +/obj/machinery/component_printer + name = "component printer" + desc = "Produces components for the creation of integrated circuits." + icon = 'icons/obj/wiremod_fab.dmi' + icon_state = "fab-idle" + circuit = /obj/item/circuitboard/machine/component_printer + + /// The internal material bus + var/datum/component/remote_materials/materials + + density = TRUE + + /// The techweb the printer will get researched designs from + var/datum/techweb/techweb + +/obj/machinery/component_printer/Initialize(mapload) + . = ..() + + techweb = SSresearch.science_tech + + materials = AddComponent( \ + /datum/component/remote_materials, \ + "component_printer", \ + mapload, \ + mat_container_flags = BREAKDOWN_FLAGS_LATHE, \ + ) + +/obj/machinery/component_printer/ui_interact(mob/user, datum/tgui/ui) + ui = SStgui.try_update_ui(user, src, ui) + if(!ui) + ui = new(user, src, "ComponentPrinter", name) + ui.open() + +/obj/machinery/component_printer/ui_assets(mob/user) + return list( + get_asset_datum(/datum/asset/spritesheet/sheetmaterials) + ) + +/obj/machinery/component_printer/ui_act(action, list/params) + . = ..() + if (.) + return + + switch (action) + if ("print") + var/design_id = params["designId"] + if (!techweb.researched_designs[design_id]) + return TRUE + + var/datum/design/design = SSresearch.techweb_design_by_id(design_id) + if (!(design.build_type & COMPONENT_PRINTER)) + return TRUE + + if (materials.on_hold()) + say("Mineral access is on hold, please contact the quartermaster.") + return TRUE + + if (!materials.mat_container?.has_materials(design.materials)) + say("Not enough materials.") + return TRUE + + balloon_alert_to_viewers("printed [design.name]") + materials.mat_container?.use_materials(design.materials) + materials.silo_log(src, "printed", -1, design.name, design.materials) + var/atom/printed_design = new design.build_path(drop_location()) + printed_design.pixel_x = printed_design.base_pixel_x + rand(-5, 5) + printed_design.pixel_y = printed_design.base_pixel_y + rand(-5, 5) + if ("remove_mat") + var/datum/material/material = locate(params["ref"]) + var/amount = text2num(params["amount"]) + + if (!amount) + return TRUE + + // SAFETY: eject_sheets checks for valid mats + materials.eject_sheets(material, amount) + + return TRUE + +/obj/machinery/component_printer/ui_data(mob/user) + var/list/data = list() + data["materials"] = materials.mat_container.ui_data() + return data + +/obj/machinery/component_printer/ui_static_data(mob/user) + var/list/data = list() + + var/list/designs = list() + + // for (var/datum/design/component/component_design_type as anything in subtypesof(/datum/design/component)) + for (var/researched_design_id in techweb.researched_designs) + var/datum/design/design = SSresearch.techweb_design_by_id(researched_design_id) + if (!(design.build_type & COMPONENT_PRINTER)) + continue + + designs[researched_design_id] = list( + "name" = design.name, + "description" = design.desc, + "materials" = get_material_cost_data(design.materials), + "categories" = design.category, + ) + + data["designs"] = designs + + return data + +/obj/machinery/component_printer/crowbar_act(mob/living/user, obj/item/tool) + if(..()) + return TRUE + return default_deconstruction_crowbar(tool) + +/obj/machinery/component_printer/screwdriver_act(mob/living/user, obj/item/tool) + if(..()) + return TRUE + return default_deconstruction_screwdriver(user, "fab-o", "fab-idle", tool) + +/obj/machinery/component_printer/proc/get_material_cost_data(list/materials) + var/list/data = list() + + for (var/datum/material/material_type as anything in materials) + data[initial(material_type.name)] = materials[material_type] + + return data + +/obj/item/circuitboard/machine/component_printer + name = "\improper Component Printer (Machine Board)" + greyscale_colors = CIRCUIT_COLOR_SCIENCE + build_path = /obj/machinery/component_printer + req_components = list( + /obj/item/stock_parts/matter_bin = 2, + /obj/item/stock_parts/manipulator = 2, + /obj/item/reagent_containers/glass/beaker = 2, + ) + +/obj/machinery/debug_component_printer + name = "debug component printer" + desc = "Produces components for the creation of integrated circuits." + icon = 'icons/obj/wiremod_fab.dmi' + icon_state = "fab-idle" + + /// All of the possible circuit designs stored by this debug printer + var/list/all_circuit_designs + + density = TRUE + +/obj/machinery/debug_component_printer/Initialize() + . = ..() + all_circuit_designs = list() + + for(var/id in SSresearch.techweb_designs) + var/datum/design/design = SSresearch.techweb_design_by_id(id) + if((design.build_type & COMPONENT_PRINTER) && design.build_path) + all_circuit_designs[design.build_path] = list( + "name" = design.name, + "description" = design.desc, + "materials" = design.materials, + "categories" = design.category + ) + + for(var/obj/item/circuit_component/component as anything in subtypesof(/obj/item/circuit_component)) + var/categories = list("Inaccessible") + if(initial(component.circuit_flags) & CIRCUIT_FLAG_ADMIN) + categories = list("Admin") + if(!(component in all_circuit_designs)) + all_circuit_designs[component] = list( + "name" = initial(component.display_name), + "description" = initial(component.desc), + "materials" = list(), + "categories" = categories, + ) + +/obj/machinery/debug_component_printer/ui_interact(mob/user, datum/tgui/ui) + ui = SStgui.try_update_ui(user, src, ui) + if(!ui) + ui = new(user, src, "ComponentPrinter", name) + ui.open() + +/obj/machinery/debug_component_printer/ui_assets(mob/user) + return list( + get_asset_datum(/datum/asset/spritesheet/sheetmaterials) + ) + +/obj/machinery/debug_component_printer/ui_act(action, list/params) + . = ..() + if (.) + return + + switch (action) + if ("print") + var/build_path = text2path(params["designId"]) + if (!build_path) + return TRUE + + var/list/design = all_circuit_designs[build_path] + if(!design) + return TRUE + + balloon_alert_to_viewers("printed [design["name"]]") + var/atom/printed_design = new build_path(drop_location()) + printed_design.pixel_x = printed_design.base_pixel_x + rand(-5, 5) + printed_design.pixel_y = printed_design.base_pixel_y + rand(-5, 5) + + return TRUE + +/obj/machinery/debug_component_printer/ui_static_data(mob/user) + var/list/data = list() + + data["materials"] = list() + data["designs"] = all_circuit_designs + + return data + +/// Module duplicator, allows you to save and recreate module components. +/obj/machinery/module_duplicator + name = "module duplicator" + desc = "Allows you to duplicate module components so that you don't have to recreate them. Scan a module component over this machine to add it as an entry." + icon = 'icons/obj/wiremod_fab.dmi' + icon_state = "module-fab-idle" + circuit = /obj/item/circuitboard/machine/module_duplicator + + /// The internal material bus + var/datum/component/remote_materials/materials + + density = TRUE + + var/list/scanned_designs = list() + + var/cost_per_component = 1000 + +/obj/machinery/module_duplicator/Initialize(mapload) + . = ..() + + materials = AddComponent( \ + /datum/component/remote_materials, \ + "module_duplicator", \ + mapload, \ + mat_container_flags = BREAKDOWN_FLAGS_LATHE, \ + ) + +/obj/machinery/module_duplicator/ui_interact(mob/user, datum/tgui/ui) + ui = SStgui.try_update_ui(user, src, ui) + if(!ui) + ui = new(user, src, "ComponentPrinter", name) + ui.open() + +/obj/machinery/module_duplicator/ui_assets(mob/user) + return list( + get_asset_datum(/datum/asset/spritesheet/sheetmaterials) + ) + +/obj/machinery/module_duplicator/ui_act(action, list/params) + . = ..() + if (.) + return + + switch (action) + if ("print") + var/design_id = text2num(params["designId"]) + + if (design_id < 1 || design_id > length(scanned_designs)) + return TRUE + + var/list/design = scanned_designs[design_id] + + if (materials.on_hold()) + say("Mineral access is on hold, please contact the quartermaster.") + return TRUE + + if (!materials.mat_container?.has_materials(design["materials"])) + say("Not enough materials.") + return TRUE + + balloon_alert_to_viewers("printed [design["name"]]") + materials.mat_container?.use_materials(design["materials"]) + materials.silo_log(src, "printed", -1, design["name"], design["materials"]) + print_module(design) + if ("remove_mat") + var/datum/material/material = locate(params["ref"]) + var/amount = text2num(params["amount"]) + + if (!amount) + return TRUE + + // SAFETY: eject_sheets checks for valid mats + materials.eject_sheets(material, amount) + + return TRUE + +/obj/machinery/module_duplicator/proc/print_module(list/design) + flick("module-fab-print", src) + addtimer(CALLBACK(src, .proc/finish_module_print, design), 1.6 SECONDS) + +/obj/machinery/module_duplicator/proc/finish_module_print(list/design) + var/obj/item/circuit_component/module/module = new(drop_location()) + module.load_data_from_list(design["dupe_data"]) + module.pixel_x = module.base_pixel_x + rand(-5, 5) + module.pixel_y = module.base_pixel_y + rand(-5, 5) + +/obj/machinery/module_duplicator/attackby(obj/item/weapon, mob/user, params) + if(!istype(weapon, /obj/item/circuit_component/module)) + return ..() + + var/obj/item/circuit_component/module/module = weapon + if(module.circuit_flags & CIRCUIT_FLAG_UNDUPEABLE) + balloon_alert(user, "module cannot be saved!") + return ..() + + if(module.display_name == initial(module.display_name)) + balloon_alert(user, "module needs a name!") + return ..() + + for(var/list/component_data as anything in scanned_designs) + if(component_data["name"] == module.display_name) + balloon_alert(user, "module name already exists!") + return ..() + + var/total_cost = 0 + for(var/obj/item/circuit_component/component as anything in module.internal_circuit.attached_components) + if(component.circuit_flags & CIRCUIT_FLAG_UNDUPEABLE) + balloon_alert(user, "module contains prohibited components!") + return ..() + + total_cost += cost_per_component + + var/list/data = list() + + data["dupe_data"] = list() + module.save_data_to_list(data["dupe_data"]) + + data["name"] = module.display_name + data["desc"] = "A module that has been loaded in by [user]." + data["materials"] = list(/datum/material/glass = total_cost) + + flick("module-fab-scan", src) + addtimer(CALLBACK(src, .proc/finish_module_scan, user, data), 1.4 SECONDS) + +/obj/machinery/module_duplicator/proc/finish_module_scan(mob/user, data) + scanned_designs += list(data) + + balloon_alert(user, "module has been saved.") + playsound(src, 'sound/machines/ping.ogg', 50) + +/obj/machinery/module_duplicator/ui_data(mob/user) + var/list/data = list() + data["materials"] = materials.mat_container.ui_data() + return data + +/obj/machinery/module_duplicator/ui_static_data(mob/user) + var/list/data = list() + + var/list/designs = list() + + var/index = 1 + for (var/list/design as anything in scanned_designs) + designs["[index]"] = list( + "name" = design["name"], + "description" = design["desc"], + "materials" = get_material_cost_data(design["materials"]), + "categories" = list("Circuitry"), + ) + index++ + + data["designs"] = designs + + return data + +/obj/machinery/module_duplicator/crowbar_act(mob/living/user, obj/item/tool) + if(..()) + return TRUE + return default_deconstruction_crowbar(tool) + +/obj/machinery/module_duplicator/screwdriver_act(mob/living/user, obj/item/tool) + if(..()) + return TRUE + return default_deconstruction_screwdriver(user, "module-fab-o", "module-fab-idle", tool) + +/obj/machinery/module_duplicator/proc/get_material_cost_data(list/materials) + var/list/data = list() + + for (var/datum/material/material_type as anything in materials) + data[initial(material_type.name)] = materials[material_type] + + return data + +/obj/item/circuitboard/machine/module_duplicator + name = "\improper Module Duplicator (Machine Board)" + greyscale_colors = CIRCUIT_COLOR_SCIENCE + build_path = /obj/machinery/module_duplicator + req_components = list( + /obj/item/stock_parts/matter_bin = 2, + /obj/item/stock_parts/manipulator = 2, + /obj/item/reagent_containers/glass/beaker = 2, + ) diff --git a/code/modules/wiremod/core/datatypes.dm b/code/modules/wiremod/core/datatypes.dm new file mode 100644 index 0000000000..b81ad0876b --- /dev/null +++ b/code/modules/wiremod/core/datatypes.dm @@ -0,0 +1,91 @@ +// An assoc list of all the possible datatypes. +GLOBAL_LIST_INIT(circuit_datatypes, generate_circuit_datatypes()) + +/proc/generate_circuit_datatypes() + var/list/datatypes_by_key = list() + for(var/datum/circuit_datatype/type as anything in subtypesof(/datum/circuit_datatype)) + if(!initial(type.datatype)) + continue + datatypes_by_key[initial(type.datatype)] = new type() + return datatypes_by_key + +/** + * A circuit datatype. Used to determine the datatype of a port and also handle any additional behaviour. + */ +/datum/circuit_datatype + /// The key. Used to identify the datatype. Should be a define. + var/datatype + + /// The color of the port in the UI. Doesn't work with hex colours. + var/color = "blue" + + /// The flags of the circuit datatype + var/datatype_flags = 0 + +/** + * Returns the value to be set for the port + * + * Used for implicit conversions between outputs and inputs (e.g. number -> string) + * and applying/removing signals on inputs + */ +/datum/circuit_datatype/proc/convert_value(datum/port/port, value_to_convert) + return value_to_convert + +/** + * Determines if a datatype is compatible with another port of a different type. + * Note: This is ALWAYS called on the input port, never on the output port. + * Inputs need to care about what types they're receiving, output ports don't have to care. + * + * Arguments: + * * datatype_to_check - The datatype to check + */ +/datum/circuit_datatype/proc/can_receive_from_datatype(datatype_to_check) + return datatype == datatype_to_check // This is already done by default on the input port. + +/** + * Called when the datatype is given to a port. + * + * Arguments: + * * gained_port - The gained port. + */ +/datum/circuit_datatype/proc/on_gain(datum/port/gained_port) + return + +/** + * Called when the datatype is removed from a port. + * + * Arguments: + * * lost_port - The removed port. + */ +/datum/circuit_datatype/proc/on_loss(datum/port/lost_port) + return + +/** + * Determines if a port is compatible with this datatype. + * This WILL throw a runtime if it returns false. This is for sanity checking and it should not return false + * unless under extraordinary circumstances or people fail to write proper code. + * + * Arguments: + * * port - The port to check if it is compatible. + */ +/datum/circuit_datatype/proc/is_compatible(datum/port/port) + return TRUE + +/** + * The data to send to the UI attached to the port. Received by the type in FUNDAMENTAL_PORT_TYPES + * + * Arguments: + * * port - The port sending the data. + */ +/datum/circuit_datatype/proc/datatype_ui_data(datum/port/port) + return + +/** + * When an input is manually set by a player. This is where extra sanitizing can happen. Will still call convert_value() + * + * Arguments: + * * port - The port sending the data. + * * + */ +/datum/circuit_datatype/proc/handle_manual_input(datum/port/input/port, mob/user, user_input) + return user_input diff --git a/code/modules/wiremod/core/duplicator.dm b/code/modules/wiremod/core/duplicator.dm new file mode 100644 index 0000000000..fd215255ba --- /dev/null +++ b/code/modules/wiremod/core/duplicator.dm @@ -0,0 +1,227 @@ +#define LOG_ERROR(list, error) if(list) { list.Add(error) } + +// Determines if a port can have a predefined input value if it is of this type. +GLOBAL_LIST_INIT(circuit_dupe_whitelisted_types, list( + PORT_TYPE_NUMBER, + PORT_TYPE_STRING, + PORT_TYPE_LIST, + PORT_TYPE_ANY, + PORT_TYPE_OPTION, +)) + +/// Loads a circuit based on json data at a location. Can also load usb connections, such as arrest consoles. +/obj/item/integrated_circuit/proc/load_circuit_data(json_data, list/errors) + var/list/general_data = json_decode(json_data) + + if(!general_data) + LOG_ERROR(errors, "Invalid json format!") + return + + if(general_data["display_name"]) + set_display_name(general_data["display_name"]) + + var/list/variable_data = general_data["variables"] + for(var/list/variable as anything in variable_data) + var/variable_name = variable["name"] + circuit_variables[variable_name] = new /datum/circuit_variable(variable_name, variable["datatype"]) + + admin_only = general_data["admin_only"] + + var/list/circuit_data = general_data["components"] + var/list/identifiers_to_circuit = list() + for(var/identifier in circuit_data) + var/list/component_data = circuit_data[identifier] + var/type = text2path(component_data["type"]) + if(!ispath(type, /obj/item/circuit_component)) + LOG_ERROR(errors, "Invalid path for circuit component, expected [/obj/item/circuit_component], got [type]") + continue + var/obj/item/circuit_component/component = load_component(type) + identifiers_to_circuit[identifier] = component + component.load_data_from_list(component_data) + + var/list/input_ports_data = component_data["input_ports_stored_data"] + for(var/port_name in input_ports_data) + var/datum/port/input/port + var/list/port_data = input_ports_data[port_name] + for(var/datum/port/input/port_to_check as anything in component.input_ports) + if(port_to_check.name == port_name) + port = port_to_check + break + + port.set_input(port_data["stored_data"]) + + var/list/external_objects = general_data["external_objects"] + for(var/identifier in external_objects) + var/list/object_data = external_objects[identifier] + var/type = text2path(object_data["type"]) + if(!ispath(type)) + LOG_ERROR(errors, "Invalid path for external object, expected a path, got [type]") + continue + var/atom/movable/object = new type(drop_location()) + var/list/connected_components = list() + for(var/component_id in object_data["connected_components"]) + var/obj/item/circuit_component/component = identifiers_to_circuit[component_id] + if(!component) + continue + connected_components += component + SEND_SIGNAL(object, COMSIG_MOVABLE_CIRCUIT_LOADED, src, connected_components) + + for(var/identifier in identifiers_to_circuit) + var/obj/item/circuit_component/component = identifiers_to_circuit[identifier] + var/list/component_data = circuit_data[identifier] + + var/list/connections = component_data["connections"] + for(var/port_name in connections) + var/datum/port/input/port + var/list/connection_data = connections[port_name] + for(var/datum/port/input/port_to_check as anything in component.input_ports) + if(port_to_check.name == port_name) + port = port_to_check + break + + if(!port) + LOG_ERROR(errors, "Port [port_name] not found for [component.type].") + continue + + if(connection_data["stored_data"]) + if(!(port.datatype in GLOB.circuit_dupe_whitelisted_types)) + continue + port.set_input(connection_data["stored_data"]) + continue + + // The || list(connected_data) is for backwards compatibility with when inputs could only be connected to up to one output. + for(var/list/output_data in (connection_data["connected_ports"] || list(connection_data))) + var/obj/item/circuit_component/connected_component = identifiers_to_circuit[output_data["component_id"]] + if(!connected_component) + LOG_ERROR(errors, "No connected component found for [component.type] for port [connection_data["port_name"]]. (connected component identifier: [connection_data["component_id"]])") + continue + + var/datum/port/output/output_port + var/output_port_name = output_data["port_name"] + for(var/datum/port/output/port_to_check as anything in connected_component.output_ports) + if(port_to_check.name == output_port_name) + output_port = port_to_check + break + + if(!output_port) + LOG_ERROR(errors, "No output port found for [component.type] for port [output_port_name] on component [connected_component.type]") + continue + + port.connect(output_port) + +#undef LOG_ERROR + +/// Converts a circuit into json. +/obj/item/integrated_circuit/proc/convert_to_json() + var/list/circuit_to_identifiers = list() + var/list/identifiers = list() + var/list/external_objects = list() // Objects that are connected to a component. These objects will be linked to the components. + for(var/obj/item/circuit_component/component as anything in attached_components) + var/identifier = "[component.type][length(identifiers)]" + identifiers += identifier + circuit_to_identifiers[component] = identifier + var/list/objects = list() + SEND_SIGNAL(component, COMSIG_CIRCUIT_COMPONENT_SAVE, objects) + + for(var/atom/movable/object as anything in objects) + if(object in external_objects) + external_objects[object] += identifier + continue + external_objects[object] = list(identifier) + + var/list/circuit_data = list() + for(var/obj/item/circuit_component/component as anything in circuit_to_identifiers) + var/identifier = circuit_to_identifiers[component] + var/list/component_data = list() + + component_data["type"] = component.type + + var/list/connections = list() + var/list/input_ports_stored_data = list() + for(var/datum/port/input/input as anything in component.input_ports) + var/list/connection_data = list() + if(!length(input.connected_ports)) + if(isnull(input.value) || !(input.datatype in GLOB.circuit_dupe_whitelisted_types)) + continue + connection_data["stored_data"] = input.value + input_ports_stored_data[input.name] = connection_data + continue + connection_data["connected_ports"] = list() + for(var/datum/port/output/output as anything in input.connected_ports) + connection_data["connected_ports"] += list(list( + "component_id" = circuit_to_identifiers[output.connected_component], + "port_name" = output.name, + )) + connections[input.name] = connection_data + component_data["connections"] = connections + component_data["input_ports_stored_data"] = input_ports_stored_data + + component.save_data_to_list(component_data) + circuit_data[identifier] = component_data + + var/external_objects_key = list() + for(var/atom/movable/object as anything in external_objects) + var/list/new_data = list() + new_data["type"] = object.type + new_data["connected_components"] = external_objects[object] + external_objects_key["[object.type][length(external_objects_key)]"] = new_data + + var/list/general_data = list() + general_data["components"] = circuit_data + general_data["external_objects"] = external_objects_key + general_data["display_name"] = display_name + general_data["admin_only"] = admin_only + + var/list/variables = list() + for(var/variable_identifier in circuit_variables) + var/list/new_data = list() + var/datum/circuit_variable/variable = circuit_variables[variable_identifier] + new_data["name"] = variable.name + new_data["datatype"] = variable.datatype + variables += list(new_data) + general_data["variables"] = variables + + return json_encode(general_data) + +/obj/item/integrated_circuit/proc/load_component(type) + var/obj/item/circuit_component/component = new type(src) + add_component(component) + return component + +/// Saves data to a list. Shouldn't be used unless you are quite literally saving the data of a component to a list. Input value is the list to save the data to +/obj/item/circuit_component/proc/save_data_to_list(list/component_data) + component_data["rel_x"] = rel_x + component_data["rel_y"] = rel_y + +/// Loads data from a list +/obj/item/circuit_component/proc/load_data_from_list(list/component_data) + rel_x = component_data["rel_x"] + rel_y = component_data["rel_y"] + +/client/proc/load_circuit() + set name = "Load Circuit" + set category = "Admin.Fun" + + if(!check_rights(R_VAREDIT)) + return + + var/list/errors = list() + + var/option = alert(usr, "Load by file or direct input?", "Load by file or string", "File", "Direct Input") + var/txt + switch(option) + if("File") + txt = file2text(tgui_input_num(usr, "Input File") as file|null) + if("Direct Input") + txt = tgui_input_num(usr, "Input JSON", "Input JSON") as text|null + + if(!txt) + return + + var/obj/item/integrated_circuit/loaded/circuit = new(mob.drop_location()) + circuit.load_circuit_data(txt, errors) + + if(length(errors)) + to_chat(src, span_warning("The following errors were found whilst compiling the circuit data:")) + for(var/error in errors) + to_chat(src, span_warning(error)) diff --git a/code/modules/wiremod/core/integrated_circuit.dm b/code/modules/wiremod/core/integrated_circuit.dm new file mode 100644 index 0000000000..f69caa7542 --- /dev/null +++ b/code/modules/wiremod/core/integrated_circuit.dm @@ -0,0 +1,600 @@ +/// A list of all integrated circuits +GLOBAL_LIST_EMPTY_TYPED(integrated_circuits, /obj/item/integrated_circuit) + +/** + * # Integrated Circuitboard + * + * A circuitboard that holds components that work together + * + * Has a limited amount of power. + */ +/obj/item/integrated_circuit + name = "integrated circuit" + desc = "By inserting components and a cell into this, wiring them up, and putting them into a shell, anyone can pretend to be a programmer." + icon = 'icons/obj/module.dmi' + icon_state = "integrated_circuit" + inhand_icon_state = "electronic" + + /// The name that appears on the shell. + var/display_name = "" + + /// The max length of the name. + var/label_max_length = 24 + + /// The power of the integrated circuit + var/obj/item/stock_parts/cell/cell + + /// The shell that this circuitboard is attached to. Used by components. + var/atom/movable/shell + + /// The attached components + var/list/obj/item/circuit_component/attached_components = list() + + /// Whether the integrated circuit is on or not. Handled by the shell. + var/on = FALSE + + /// Whether the integrated circuit is locked or not. Handled by the shell. + var/locked = FALSE + + /// Whether the integrated circuit is admin only. Disables power usage and allows admin circuits to be attached, at the cost of making it inaccessible to regular users. + var/admin_only = FALSE + + /// The ID that is authorized to unlock/lock the shell so that the circuit can/cannot be removed. + var/datum/weakref/owner_id + + /// The current examined component. Used in IntegratedCircuit UI + var/datum/weakref/examined_component + + /// Set by the shell. Holds the reference to the owner who inserted the component into the shell. + var/datum/weakref/inserter_mind + + /// Variables stored on this integrated circuit. with a `variable_name = value` structure + var/list/datum/circuit_variable/circuit_variables = list() + + /// The maximum amount of setters and getters a circuit can have + var/max_setters_and_getters = 30 + + /// The current setter and getter count the circuit has. + var/setter_and_getter_count = 0 + + /// X position of the examined_component + var/examined_rel_x = 0 + + /// Y position of the examined component + var/examined_rel_y = 0 + + /// The X position of the screen. Used for adding components + var/screen_x = 0 + + /// The Y position of the screen. Used for adding components. + var/screen_y = 0 + +/obj/item/integrated_circuit/Initialize() + . = ..() + + GLOB.integrated_circuits += src + + RegisterSignal(src, COMSIG_ATOM_USB_CABLE_TRY_ATTACH, .proc/on_atom_usb_cable_try_attach) + +/obj/item/integrated_circuit/loaded/Initialize() + . = ..() + set_cell(new /obj/item/stock_parts/cell/high(src)) + +/obj/item/integrated_circuit/Destroy() + for(var/obj/item/circuit_component/to_delete in attached_components) + remove_component(to_delete) + qdel(to_delete) + QDEL_LIST(circuit_variables) + attached_components.Cut() + shell = null + examined_component = null + owner_id = null + QDEL_NULL(cell) + GLOB.integrated_circuits -= src + return ..() + +/obj/item/integrated_circuit/examine(mob/user) + . = ..() + if(cell) + . += span_notice("The charge meter reads [cell ? round(cell.percent(), 1) : 0]%.") + else + . += span_notice("There is no power cell installed.") + +/obj/item/integrated_circuit/proc/set_cell(obj/item/stock_parts/cell_to_set) + SEND_SIGNAL(src, COMSIG_CIRCUIT_SET_CELL, cell_to_set) + cell = cell_to_set + +/obj/item/integrated_circuit/attackby(obj/item/I, mob/living/user, params) + . = ..() + if(istype(I, /obj/item/circuit_component)) + add_component_manually(I, user) + return + + if(istype(I, /obj/item/stock_parts/cell)) + if(cell) + balloon_alert(user, "there already is a cell inside!") + return + if(!user.transferItemToLoc(I, src)) + return + set_cell(I) + I.add_fingerprint(user) + user.visible_message(span_notice("[user] inserts a power cell into [src]."), span_notice("You insert the power cell into [src].")) + return + + if(istype(I, /obj/item/card/id)) + balloon_alert(user, "owner id set for [I]") + owner_id = WEAKREF(I) + return + + if(I.tool_behaviour == TOOL_SCREWDRIVER) + if(!cell) + return + I.play_tool_sound(src) + user.visible_message(span_notice("[user] unscrews the power cell from [src]."), span_notice("You unscrew the power cell from [src].")) + cell.forceMove(drop_location()) + set_cell(null) + return + +/** + * Registers an movable atom as a shell + * + * No functionality is done here. This is so that input components + * can properly register any signals on the shell. + * Arguments: + * * new_shell - The new shell to register. + */ +/obj/item/integrated_circuit/proc/set_shell(atom/movable/new_shell) + remove_current_shell() + set_on(TRUE) + SEND_SIGNAL(src, COMSIG_CIRCUIT_SET_SHELL, new_shell) + shell = new_shell + RegisterSignal(shell, COMSIG_PARENT_QDELETING, .proc/remove_current_shell) + for(var/obj/item/circuit_component/attached_component as anything in attached_components) + attached_component.register_shell(shell) + // Their input ports may be updated with user values, but the outputs haven't updated + // because on is FALSE + TRIGGER_CIRCUIT_COMPONENT(attached_component, null) + +/** + * Unregisters the current shell attached to this circuit. + */ +/obj/item/integrated_circuit/proc/remove_current_shell() + SIGNAL_HANDLER + if(!shell) + return + shell.name = initial(shell.name) + for(var/obj/item/circuit_component/attached_component as anything in attached_components) + attached_component.unregister_shell(shell) + UnregisterSignal(shell, COMSIG_PARENT_QDELETING) + shell = null + set_on(FALSE) + SEND_SIGNAL(src, COMSIG_CIRCUIT_SHELL_REMOVED) + +/obj/item/integrated_circuit/proc/set_on(new_value) + SEND_SIGNAL(src, COMSIG_CIRCUIT_SET_ON, new_value) + on = new_value + +/** + * Adds a component to the circuitboard + * + * Once the component is added, the ports can be attached to other components + */ +/obj/item/integrated_circuit/proc/add_component(obj/item/circuit_component/to_add, mob/living/user) + if(to_add.parent) + return + + if(SEND_SIGNAL(src, COMSIG_CIRCUIT_ADD_COMPONENT, to_add, user) & COMPONENT_CANCEL_ADD_COMPONENT) + return + + if(!to_add.add_to(src)) + return + + var/success = FALSE + if(user) + success = user.transferItemToLoc(to_add, src) + else + success = to_add.forceMove(src) + + if(!success) + return + + to_add.rel_x = rand(COMPONENT_MIN_RANDOM_POS, COMPONENT_MAX_RANDOM_POS) - screen_x + to_add.rel_y = rand(COMPONENT_MIN_RANDOM_POS, COMPONENT_MAX_RANDOM_POS) - screen_y + to_add.parent = src + attached_components += to_add + RegisterSignal(to_add, COMSIG_MOVABLE_MOVED, .proc/component_move_handler) + SStgui.update_uis(src) + + if(shell) + to_add.register_shell(shell) + return TRUE + +/** + * Adds a component to the circuitboard through a manual action. + */ +/obj/item/integrated_circuit/proc/add_component_manually(obj/item/circuit_component/to_add, mob/living/user) + if (SEND_SIGNAL(src, COMSIG_CIRCUIT_ADD_COMPONENT_MANUALLY, to_add, user) & COMPONENT_CANCEL_ADD_COMPONENT) + return + + return add_component(to_add, user) + +/obj/item/integrated_circuit/proc/component_move_handler(obj/item/circuit_component/source) + SIGNAL_HANDLER + if(source.loc != src) + remove_component(source) + +/** + * Removes a component to the circuitboard + * + * This removes all connects between the ports + */ +/obj/item/integrated_circuit/proc/remove_component(obj/item/circuit_component/to_remove) + if(shell) + to_remove.unregister_shell(shell) + + UnregisterSignal(to_remove, COMSIG_MOVABLE_MOVED) + attached_components -= to_remove + to_remove.disconnect() + to_remove.parent = null + SEND_SIGNAL(to_remove, COMSIG_CIRCUIT_COMPONENT_REMOVED, src) + SStgui.update_uis(src) + to_remove.removed_from(src) + +/obj/item/integrated_circuit/get_cell() + return cell + +/obj/item/integrated_circuit/ui_assets(mob/user) + return list( + get_asset_datum(/datum/asset/simple/circuit_assets) + ) + +/obj/item/integrated_circuit/ui_static_data(mob/user) + . = list() + .["global_basic_types"] = GLOB.wiremod_basic_types + .["screen_x"] = screen_x + .["screen_y"] = screen_y + +/obj/item/integrated_circuit/ui_data(mob/user) + . = list() + .["components"] = list() + for(var/obj/item/circuit_component/component as anything in attached_components) + if (component.circuit_flags & CIRCUIT_FLAG_HIDDEN) + .["components"] += null + continue + + var/list/component_data = list() + component_data["input_ports"] = list() + for(var/datum/port/input/port as anything in component.input_ports) + var/current_data = port.value + if(isatom(current_data)) // Prevent passing the name of the atom. + current_data = null + var/list/connected_to = list() + for(var/datum/port/output/output as anything in port.connected_ports) + connected_to += REF(output) + component_data["input_ports"] += list(list( + "name" = port.name, + "type" = port.datatype, + "ref" = REF(port), // The ref is the identifier to work out what it is connected to + "connected_to" = connected_to, + "color" = port.color, + "current_data" = current_data, + "datatype_data" = port.datatype_ui_data(user), + )) + component_data["output_ports"] = list() + for(var/datum/port/output/port as anything in component.output_ports) + component_data["output_ports"] += list(list( + "name" = port.name, + "type" = port.datatype, + "ref" = REF(port), + "color" = port.color, + )) + + component_data["name"] = component.display_name + component_data["x"] = component.rel_x + component_data["y"] = component.rel_y + component_data["removable"] = component.removable + .["components"] += list(component_data) + + .["variables"] = list() + for(var/variable_name in circuit_variables) + var/datum/circuit_variable/variable = circuit_variables[variable_name] + var/list/variable_data = list() + variable_data["name"] = variable.name + variable_data["datatype"] = variable.datatype + variable_data["color"] = variable.color + .["variables"] += list(variable_data) + + + .["display_name"] = display_name + + var/obj/item/circuit_component/examined + if(examined_component) + examined = examined_component.resolve() + + .["examined_name"] = examined?.display_name + .["examined_desc"] = examined?.desc + .["examined_notices"] = examined?.get_ui_notices() + .["examined_rel_x"] = examined_rel_x + .["examined_rel_y"] = examined_rel_y + + .["is_admin"] = check_rights_for(user.client, R_VAREDIT) + +/obj/item/integrated_circuit/ui_host(mob/user) + if(shell) + return shell + return ..() + +/obj/item/integrated_circuit/can_interact(mob/user) + if(locked) + return FALSE + return ..() + +/obj/item/integrated_circuit/ui_status(mob/user) + . = ..() + + if (isobserver(user)) + . = max(., UI_UPDATE) + + // Extra protection because ui_state will not close the UI if they already have the ui open, + // as ui_state is only set during + if(admin_only) + if(!check_rights_for(user.client, R_VAREDIT)) + return UI_CLOSE + else + return UI_INTERACTIVE + +/obj/item/integrated_circuit/ui_state(mob/user) + if(!shell) + return GLOB.hands_state + return GLOB.physical_obscured_state + +/obj/item/integrated_circuit/ui_interact(mob/user, datum/tgui/ui) + ui = SStgui.try_update_ui(user, src, ui) + if(!ui) + ui = new(user, src, "IntegratedCircuit", name) + ui.open() + ui.set_autoupdate(FALSE) + +#define WITHIN_RANGE(id, table) (id >= 1 && id <= length(table)) + +/obj/item/integrated_circuit/ui_act(action, list/params) + . = ..() + if(.) + return + + switch(action) + if("add_connection") + var/input_component_id = text2num(params["input_component_id"]) + var/output_component_id = text2num(params["output_component_id"]) + var/input_port_id = text2num(params["input_port_id"]) + var/output_port_id = text2num(params["output_port_id"]) + if(!WITHIN_RANGE(input_component_id, attached_components) || !WITHIN_RANGE(output_component_id, attached_components)) + return + var/obj/item/circuit_component/input_component = attached_components[input_component_id] + var/obj/item/circuit_component/output_component = attached_components[output_component_id] + + if(!WITHIN_RANGE(input_port_id, input_component.input_ports) || !WITHIN_RANGE(output_port_id, output_component.output_ports)) + return + var/datum/port/input/input_port = input_component.input_ports[input_port_id] + var/datum/port/output/output_port = output_component.output_ports[output_port_id] + + if(!input_port.can_receive_from_datatype(output_port.datatype)) + return + input_port.connect(output_port) + . = TRUE + if("remove_connection") + var/component_id = text2num(params["component_id"]) + var/is_input = params["is_input"] + var/port_id = text2num(params["port_id"]) + + if(!WITHIN_RANGE(component_id, attached_components)) + return + var/obj/item/circuit_component/component = attached_components[component_id] + + var/list/port_table + if(is_input) + port_table = component.input_ports + else + port_table = component.output_ports + + if(!WITHIN_RANGE(port_id, port_table)) + return + + var/datum/port/port = port_table[port_id] + port.disconnect_all() + . = TRUE + if("detach_component") + var/component_id = text2num(params["component_id"]) + if(!WITHIN_RANGE(component_id, attached_components)) + return + var/obj/item/circuit_component/component = attached_components[component_id] + if(!component.removable) + return + component.disconnect() + remove_component(component) + if(component.loc == src) + usr.put_in_hands(component) + . = TRUE + if("set_component_coordinates") + var/component_id = text2num(params["component_id"]) + if(!WITHIN_RANGE(component_id, attached_components)) + return + var/obj/item/circuit_component/component = attached_components[component_id] + component.rel_x = min(max(-COMPONENT_MAX_POS, text2num(params["rel_x"])), COMPONENT_MAX_POS) + component.rel_y = min(max(-COMPONENT_MAX_POS, text2num(params["rel_y"])), COMPONENT_MAX_POS) + . = TRUE + if("set_component_input") + var/component_id = text2num(params["component_id"]) + var/port_id = text2num(params["port_id"]) + if(!WITHIN_RANGE(component_id, attached_components)) + return + var/obj/item/circuit_component/component = attached_components[component_id] + if(!WITHIN_RANGE(port_id, component.input_ports)) + return + var/datum/port/input/port = component.input_ports[port_id] + + if(params["set_null"]) + port.set_input(null) + return TRUE + + if(params["marked_atom"]) + if(port.datatype != PORT_TYPE_ATOM && port.datatype != PORT_TYPE_ANY) + return + var/obj/item/multitool/circuit/marker = usr.get_active_held_item() + if(!istype(marker)) + var/client/user = usr.client + if(!check_rights_for(user, R_VAREDIT)) + return TRUE + var/atom/marked_atom = user.holder.marked_datum + if(!marked_atom) + return TRUE + port.set_input(marked_atom) + balloon_alert(usr, "updated [port.name]'s value to marked object.") + return TRUE + if(!marker.marked_atom) + port.set_input(null) + marker.say("Cleared port ('[port.name]')'s value.") + return TRUE + marker.say("Updated port ('[port.name]')'s value to the marked entity.") + port.set_input(marker.marked_atom) + return TRUE + + var/user_input = port.handle_manual_input(usr, params["input"]) + if(isnull(user_input)) + return TRUE + port.set_input(user_input) + . = TRUE + if("get_component_value") + var/component_id = text2num(params["component_id"]) + var/port_id = text2num(params["port_id"]) + if(!WITHIN_RANGE(component_id, attached_components)) + return + var/obj/item/circuit_component/component = attached_components[component_id] + if(!WITHIN_RANGE(port_id, component.output_ports)) + return + + var/datum/port/output/port = component.output_ports[port_id] + var/value = port.value + if(isatom(value)) + value = PORT_TYPE_ATOM + else if(isnull(value)) + value = "null" + var/string_form = copytext("[value]", 1, PORT_MAX_STRING_DISPLAY) + if(length(string_form) >= PORT_MAX_STRING_DISPLAY-1) + string_form += "..." + balloon_alert(usr, "[port.name] value: [string_form]") + . = TRUE + if("set_display_name") + var/new_name = params["display_name"] + + if(new_name) + set_display_name(strip_html(params["display_name"], label_max_length)) + else + set_display_name("") + + if(shell) + if(display_name != "") + shell.name = "[initial(shell.name)] ([display_name])" + else + shell.name = initial(shell.name) + + . = TRUE + if("set_examined_component") + var/component_id = text2num(params["component_id"]) + if(!WITHIN_RANGE(component_id, attached_components)) + return + examined_component = WEAKREF(attached_components[component_id]) + examined_rel_x = text2num(params["x"]) + examined_rel_y = text2num(params["y"]) + . = TRUE + if("remove_examined_component") + examined_component = null + . = TRUE + if("save_circuit") + return attempt_save_to(usr.client) + if("add_variable") + var/variable_identifier = trim(copytext(params["variable_name"], 1, PORT_MAX_NAME_LENGTH)) + if(variable_identifier in circuit_variables) + return TRUE + if(variable_identifier == "") + return TRUE + var/variable_datatype = params["variable_datatype"] + if(!(variable_datatype in GLOB.wiremod_basic_types)) + return + + circuit_variables[variable_identifier] = new /datum/circuit_variable(variable_identifier, variable_datatype) + . = TRUE + if("remove_variable") + var/variable_identifier = params["variable_name"] + if(!(variable_identifier in circuit_variables)) + return + var/datum/circuit_variable/variable = circuit_variables[variable_identifier] + if(!variable) + return + circuit_variables -= variable_identifier + qdel(variable) + . = TRUE + if("add_setter_or_getter") + if(setter_and_getter_count >= max_setters_and_getters) + balloon_alert(usr, "setter and getter count at maximum capacity") + return + var/designated_type = /obj/item/circuit_component/getter + if(params["is_setter"]) + designated_type = /obj/item/circuit_component/setter + var/obj/item/circuit_component/component = new designated_type(src) + if(!add_component(component, usr)) + qdel(component) + return + RegisterSignal(component, COMSIG_CIRCUIT_COMPONENT_REMOVED, .proc/clear_setter_or_getter) + setter_and_getter_count++ + if("move_screen") + screen_x = text2num(params["screen_x"]) + screen_y = text2num(params["screen_y"]) + +/obj/item/integrated_circuit/proc/clear_setter_or_getter(datum/source) + SIGNAL_HANDLER + // This'll also be called in the Destroy() override of /obj/item/circuit_component + if(!QDELING(source)) + qdel(source) + setter_and_getter_count-- + +/obj/item/integrated_circuit/proc/on_atom_usb_cable_try_attach(datum/source, obj/item/usb_cable/usb_cable, mob/user) + SIGNAL_HANDLER + usb_cable.balloon_alert(user, "circuit needs to be in a compatible shell") + return COMSIG_CANCEL_USB_CABLE_ATTACK + +#undef WITHIN_RANGE + +/// Sets the display name that appears on the shell. +/obj/item/integrated_circuit/proc/set_display_name(new_name) + display_name = new_name + +/** + * Returns the creator of the integrated circuit. Used in admin messages and other related things. + */ +/obj/item/integrated_circuit/proc/get_creator_admin() + return get_creator(include_link = TRUE) + +/** + * Returns the creator of the integrated circuit. Used in admin logs and other related things. + */ +/obj/item/integrated_circuit/proc/get_creator(include_link = FALSE) + var/datum/mind/inserter + if(inserter_mind) + inserter = inserter_mind.resolve() + + var/obj/item/card/id/id_card + if(owner_id) + id_card = owner_id.resolve() + + return "[src] (Shell: [shell || "*null*"], Inserter: [key_name(inserter, include_link)], Owner ID: [id_card?.name || "*null*"])" + +/// Attempts to save a circuit to a given client +/obj/item/integrated_circuit/proc/attempt_save_to(client/saver) + if(!check_rights_for(saver, R_VAREDIT)) + return FALSE + var/temp_file = file("data/CircuitDownloadTempFile") + fdel(temp_file) + WRITE_FILE(temp_file, convert_to_json()) + DIRECT_OUTPUT(saver, ftp(temp_file, "[display_name || "circuit"].json")) + return TRUE diff --git a/code/modules/wiremod/core/marker.dm b/code/modules/wiremod/core/marker.dm new file mode 100644 index 0000000000..82d34be37a --- /dev/null +++ b/code/modules/wiremod/core/marker.dm @@ -0,0 +1,60 @@ +/obj/item/multitool/circuit + name = "circuit multitool" + desc = "A circuit multitool. Used to mark entities which can then be uploaded to components by pressing the upload button on a port. \ + Acts as a normal multitool otherwise. Use in hand to clear marked entity so that you can mark another entity." + icon_state = "multitool_circuit" + + /// The marked atom of this multitool + var/atom/marked_atom + +/obj/item/multitool/circuit/Destroy() + marked_atom = null + return ..() + +/obj/item/multitool/circuit/examine(mob/user) + . = ..() + . += span_notice("It has [marked_atom? "a" : "no"] marked entity registered.") + +/obj/item/multitool/circuit/attack_self(mob/user, modifiers) + . = ..() + if(.) + return + if(!marked_atom) + return + + say("Cleared marked targets.") + clear_marked_atom() + return TRUE + +/obj/item/multitool/circuit/melee_attack_chain(mob/user, atom/target, params) + var/is_right_clicking = LAZYACCESS(params2list(params), RIGHT_CLICK) + + if(marked_atom || !user.Adjacent(target) || is_right_clicking) + return ..() + + say("Marked [target].") + marked_atom = target + RegisterSignal(marked_atom, COMSIG_PARENT_QDELETING, .proc/cleanup_marked_atom) + update_icon() + flick("multitool_circuit_flick", src) + playsound(src.loc, 'sound/misc/compiler-stage2.ogg', 30, TRUE) + return TRUE + +/obj/item/multitool/circuit/update_overlays() + . = ..() + cut_overlays() + if(marked_atom) + . += "marked_overlay" + +/// Clears the current marked atom +/obj/item/multitool/circuit/proc/clear_marked_atom() + if(!marked_atom) + return + UnregisterSignal(marked_atom, COMSIG_PARENT_QDELETING) + marked_atom = null + update_icon() + +/obj/item/multitool/circuit/proc/cleanup_marked_atom(datum/source) + SIGNAL_HANDLER + if(source == marked_atom) + clear_marked_atom() diff --git a/code/modules/wiremod/core/port.dm b/code/modules/wiremod/core/port.dm new file mode 100644 index 0000000000..a58016743f --- /dev/null +++ b/code/modules/wiremod/core/port.dm @@ -0,0 +1,216 @@ +/** + * # Component Port + * + * A port used by a component. Connects to other ports. + */ +/datum/port + /// The component this port is attached to + var/obj/item/circuit_component/connected_component + + /// Name of the port. Used when displaying the port. + var/name + + /// The port type. Ports can only connect to each other if the type matches + var/datatype + + /// The value that's currently in the port. It's of the above type. + var/value + + /// The default port type. Stores the original datatype of the port set on Initialize. + var/datum/circuit_datatype/datatype_handler + + /// The port color. If unset, appears as blue. + var/color + +/datum/port/New(obj/item/circuit_component/to_connect, name, datatype) + if(!to_connect) + qdel(src) + return + . = ..() + connected_component = to_connect + src.name = name + set_datatype(datatype) + +/datum/port/Destroy(force) + disconnect_all() + connected_component = null + datatype_handler = null + return ..() + +/** + * Sets the port's value to value. + * Casts to the port's datatype (e.g. number -> string), and assumes this can be done. + */ +/datum/port/proc/set_value(value, force = FALSE) + if(src.value != value || force) + if(isatom(value)) + UnregisterSignal(value, COMSIG_PARENT_QDELETING) + src.value = datatype_handler.convert_value(src, value) + if(isatom(value)) + RegisterSignal(value, COMSIG_PARENT_QDELETING, .proc/null_value) + SEND_SIGNAL(src, COMSIG_PORT_SET_VALUE, value) + +/** + * Updates the value of the input and calls input_received on the connected component + */ +/datum/port/input/proc/set_input(value) + if(QDELETED(src)) //Pain + return + set_value(value) + if(trigger) + TRIGGER_CIRCUIT_COMPONENT(connected_component, src) + +/datum/port/output/proc/set_output(value) + set_value(value) + +/** + * Sets the datatype of the port. + * + * Arguments: + * * new_type - The type this port is to be set to. + */ +/datum/port/proc/set_datatype(type_to_set) + if(type_to_set == datatype) + return + + if(datatype_handler) + datatype_handler.on_loss(src) + datatype_handler = null + + var/datum/circuit_datatype/handler = GLOB.circuit_datatypes[type_to_set] + if(!handler || !handler.is_compatible(src)) + type_to_set = PORT_TYPE_ANY + handler = GLOB.circuit_datatypes[type_to_set] + // We can't leave this port without a type or else it'll just keep spewing out unnecessary and unneeded runtimes as well as leaving the circuit in a broken state. + stack_trace("[src] port attempted to be set to an incompatible datatype! (target datatype to set: [type_to_set])") + + datatype = type_to_set + datatype_handler = handler + color = datatype_handler.color + datatype_handler.on_gain(src) + src.value = datatype_handler.convert_value(src, value) + SEND_SIGNAL(src, COMSIG_PORT_SET_TYPE, type_to_set) + if(connected_component?.parent) + SStgui.update_uis(connected_component.parent) + +/datum/port/input/set_datatype(new_type) + for(var/datum/port/output/output as anything in connected_ports) + check_type(output) + ..() + +/** + * Returns the data from the datatype + */ +/datum/port/proc/datatype_ui_data() + return datatype_handler.datatype_ui_data(src) + +/** + * # Output Port + * + * An output port that many input ports can connect to + * + * Sends a signal whenever the output value is changed + */ +/datum/port/output + +/** + * Disconnects a port from all other ports. + * + * Called by [/obj/item/circuit_component] whenever it is disconnected from + * an integrated circuit + */ +/datum/port/proc/disconnect_all() + SEND_SIGNAL(src, COMSIG_PORT_DISCONNECT) + +/datum/port/input/disconnect_all() + ..() + for(var/datum/port/output/output as anything in connected_ports) + disconnect(output) + +/datum/port/input/proc/disconnect(datum/port/output/output) + connected_ports -= output + UnregisterSignal(output, COMSIG_PORT_SET_VALUE) + UnregisterSignal(output, COMSIG_PORT_SET_TYPE) + UnregisterSignal(output, COMSIG_PORT_DISCONNECT) + +/// Do our part in setting all source references anywhere to null. +/datum/port/proc/on_value_qdeleting(datum/source) + SIGNAL_HANDLER + if(value == source) + value = null + else + stack_trace("Impossible? [src] should only receive COMSIG_PARENT_QDELETING from an atom currently in the port, not [source].") + +/** + * # Input Port + * + * An input port remembers connected output ports. + * + * Registers the PORT_SET_VALUE signal on each connected port, + * and keeps its value equal to the last such signal received. + */ +/datum/port/input + /// Whether this port triggers an update whenever an output is received. + var/trigger = FALSE + + /// The ports this port is wired to. + var/list/datum/port/output/connected_ports + +/datum/port/input/New(obj/item/circuit_component/to_connect, name, datatype, trigger, default) + . = ..() + set_value(default) + src.trigger = trigger + src.connected_ports = list() + +/** + * Introduces two ports to one another. + */ +/datum/port/input/proc/connect(datum/port/output/output) + connected_ports |= output + RegisterSignal(output, COMSIG_PORT_SET_VALUE, .proc/receive_value) + RegisterSignal(output, COMSIG_PORT_SET_TYPE, .proc/check_type) + RegisterSignal(output, COMSIG_PORT_DISCONNECT, .proc/disconnect) + // For signals, we don't update the input to prevent sending a signal when connecting ports. + if(!(datatype_handler.datatype_flags & DATATYPE_FLAG_AVOID_VALUE_UPDATE)) + set_input(output.value) + +/** + * Determines if a datatype is compatible with another port of a different type. + * + * Arguments: + * * other_datatype - The datatype to check + */ +/datum/port/input/proc/can_receive_from_datatype(datatype_to_check) + return datatype_handler.can_receive_from_datatype(datatype_to_check) + +/** + * Determines if a datatype is compatible with another port of a different type. + * + * Arguments: + * * other_datatype - The datatype to check + */ +/datum/port/input/proc/handle_manual_input(mob/user, manual_input) + if(datatype_handler.datatype_flags & DATATYPE_FLAG_ALLOW_MANUAL_INPUT) + return datatype_handler.handle_manual_input(src, user, manual_input) + return null + +/** + * Mirror value updates from connected output ports after an input_receive_delay. + */ +/datum/port/input/proc/receive_value(datum/port/output/output, value) + SIGNAL_HANDLER + SScircuit_component.add_callback(CALLBACK(src, .proc/set_input, value)) + +/// Signal handler proc to null the input if an atom is deleted. An update is not sent because this was not set by anything. +/datum/port/proc/null_value(datum/source) + SIGNAL_HANDLER + if(value == source) + value = null + +/** + * Handle type updates from connected output ports, breaking uncastable connections. + */ +/datum/port/input/proc/check_type(datum/port/output/output) + SIGNAL_HANDLER + if(!can_receive_from_datatype(output.datatype)) + disconnect(output) diff --git a/code/modules/wiremod/core/usb_cable.dm b/code/modules/wiremod/core/usb_cable.dm new file mode 100644 index 0000000000..df0dec936c --- /dev/null +++ b/code/modules/wiremod/core/usb_cable.dm @@ -0,0 +1,123 @@ +/// A cable that can connect integrated circuits to anything with a USB port, such as computers and machines. +/obj/item/usb_cable + name = "usb cable" + desc = "A cable that can connect integrated circuits to anything with a USB port, such as computers and machines." + icon = 'icons/obj/wiremod.dmi' + icon_state = "usb_cable" + inhand_icon_state = "coil" + base_icon_state = "coil" + w_class = WEIGHT_CLASS_TINY + custom_materials = list(/datum/material/iron = 75) + + /// The currently connected circuit + var/obj/item/integrated_circuit/attached_circuit + +/obj/item/usb_cable/Destroy() + attached_circuit = null + STOP_PROCESSING(SSobj, src) + return ..() + +/obj/item/usb_cable/Initialize() + . = ..() + RegisterSignal(src, COMSIG_MOVABLE_MOVED, .proc/on_moved) + +/obj/item/usb_cable/examine(mob/user) + . = ..() + + if (!isnull(attached_circuit)) + . += span_notice("It is attached to [attached_circuit.shell || attached_circuit].") + +// Look, I'm not happy about this either, but moving an object doesn't call Moved if it's inside something else. +// There's good reason for this, but there's no element or similar yet to track it as far as I know. +// SSobj runs infrequently, this is only ran while there's an attached circuit, its performance cost is negligible. +/obj/item/usb_cable/process(delta_time) + if (!check_in_range()) + return PROCESS_KILL + +/obj/item/usb_cable/pre_attack(atom/target, mob/living/user, params) + . = ..() + if (.) + return + + if (prob(1)) + balloon_alert(user, "wrong way, god damnit") + return TRUE + + var/signal_result = SEND_SIGNAL(target, COMSIG_ATOM_USB_CABLE_TRY_ATTACH, src, user) + + var/last_attached_circuit = attached_circuit + if (signal_result & COMSIG_USB_CABLE_CONNECTED_TO_CIRCUIT) + if (isnull(attached_circuit)) + CRASH("Producers of COMSIG_USB_CABLE_CONNECTED_TO_CIRCUIT must set attached_circuit") + balloon_alert(user, "connected to circuit\nconnect to a port") + + playsound(src, 'sound/machines/pda_button1.ogg', 20, TRUE) + + if (last_attached_circuit != attached_circuit) + if (!isnull(last_attached_circuit)) + unregister_circuit_signals(last_attached_circuit) + register_circuit_signals() + + START_PROCESSING(SSobj, src) + + return TRUE + + if (signal_result & COMSIG_USB_CABLE_ATTACHED) + // Short messages are better to read + var/connection_description = "port" + if (istype(target, /obj/machinery/computer)) + connection_description = "computer" + else if (ismachinery(target)) + connection_description = "machine" + + balloon_alert(user, "connected to [connection_description]") + playsound(src, 'sound/items/screwdriver2.ogg', 20, TRUE) + + return TRUE + + if (signal_result & COMSIG_CANCEL_USB_CABLE_ATTACK) + return TRUE + + return FALSE + +/obj/item/usb_cable/suicide_act(mob/user) + user.visible_message(span_suicide("[user] is wrapping [src] around [user.p_their()] neck! It looks like [user.p_theyre()] trying to commit suicide!")) + return OXYLOSS + +/obj/item/usb_cable/proc/register_circuit_signals() + RegisterSignal(attached_circuit, COMSIG_MOVABLE_MOVED, .proc/on_moved) + RegisterSignal(attached_circuit, COMSIG_PARENT_QDELETING, .proc/on_circuit_qdeling) + RegisterSignal(attached_circuit.shell, COMSIG_MOVABLE_MOVED, .proc/on_moved) + +/obj/item/usb_cable/proc/unregister_circuit_signals(obj/item/integrated_circuit/old_circuit) + UnregisterSignal(attached_circuit, list( + COMSIG_MOVABLE_MOVED, + COMSIG_PARENT_QDELETING, + )) + + UnregisterSignal(attached_circuit.shell, COMSIG_MOVABLE_MOVED) + +/obj/item/usb_cable/proc/on_moved() + SIGNAL_HANDLER + + check_in_range() + +/obj/item/usb_cable/proc/check_in_range() + if (isnull(attached_circuit)) + STOP_PROCESSING(SSobj, src) + return FALSE + + if (!IN_GIVEN_RANGE(attached_circuit, src, USB_CABLE_MAX_RANGE)) + balloon_alert_to_viewers("detached, too far away") + unregister_circuit_signals(attached_circuit) + attached_circuit = null + STOP_PROCESSING(SSobj, src) + return FALSE + + return TRUE + +/obj/item/usb_cable/proc/on_circuit_qdeling() + SIGNAL_HANDLER + + attached_circuit = null + STOP_PROCESSING(SSobj, src) diff --git a/code/modules/wiremod/core/variable.dm b/code/modules/wiremod/core/variable.dm new file mode 100644 index 0000000000..62d5239ade --- /dev/null +++ b/code/modules/wiremod/core/variable.dm @@ -0,0 +1,49 @@ +/** + * A circuit variable that holds the name, the datatype and the colour of the variable (taken from the datatype). + * + * Used in integrated circuits for setter and getter circuit components. + */ +/datum/circuit_variable + /// The display name of the circuit variable + var/name + + /// The datatype of the circuit variable. Used by the setter and getter circuit components + var/datatype + + /// The colour that appears in the UI. The value is set to the datatype's matching colour + var/color + + /// The current value held by the variable. + var/value + + /// The components that are currently listening. Triggers them when the value is updated. + var/list/obj/item/circuit_component/listeners + +/datum/circuit_variable/New(name, datatype) + . = ..() + src.name = name + src.datatype = datatype + + var/datum/circuit_datatype/circuit_datatype = GLOB.circuit_datatypes[datatype] + + src.listeners = list() + src.color = circuit_datatype.color + + +/datum/circuit_variable/Destroy(force, ...) + listeners = null + return ..() + +/// Sets the value of the circuit component and triggers the appropriate listeners +/datum/circuit_variable/proc/set_value(new_value) + value = new_value + for(var/obj/item/circuit_component/component as anything in listeners) + TRIGGER_CIRCUIT_COMPONENT(component, null) + +/// Adds a listener to receive inputs when the variable has a value that is set. +/datum/circuit_variable/proc/add_listener(obj/item/circuit_component/to_add) + listeners += to_add + +/// Removes a listener to receive inputs when the variable has a value that is set. Listener will usually clean themselves up +/datum/circuit_variable/proc/remove_listener(obj/item/circuit_component/to_remove) + listeners -= to_remove diff --git a/code/modules/wiremod/datatypes/any.dm b/code/modules/wiremod/datatypes/any.dm new file mode 100644 index 0000000000..a8a2f8457f --- /dev/null +++ b/code/modules/wiremod/datatypes/any.dm @@ -0,0 +1,10 @@ +/datum/circuit_datatype/any + datatype = PORT_TYPE_ANY + color = "blue" + datatype_flags = DATATYPE_FLAG_ALLOW_MANUAL_INPUT + +/datum/circuit_datatype/any/can_receive_from_datatype(datatype_to_check) + return TRUE + +/datum/circuit_datatype/any/handle_manual_input(datum/port/input/port, mob/user, user_input) + return text2num(user_input) || user_input diff --git a/code/modules/wiremod/datatypes/basic.dm b/code/modules/wiremod/datatypes/basic.dm new file mode 100644 index 0000000000..d112f2b9d9 --- /dev/null +++ b/code/modules/wiremod/datatypes/basic.dm @@ -0,0 +1,9 @@ +// This file is for types that do not have any special conversion behaviour + +/datum/circuit_datatype/list_type + datatype = PORT_TYPE_LIST + color = "white" + +/datum/circuit_datatype/table + datatype = PORT_TYPE_TABLE + color = "grey" diff --git a/code/modules/wiremod/datatypes/entity.dm b/code/modules/wiremod/datatypes/entity.dm new file mode 100644 index 0000000000..44e00b9c6c --- /dev/null +++ b/code/modules/wiremod/datatypes/entity.dm @@ -0,0 +1,10 @@ +/datum/circuit_datatype/entity + datatype = PORT_TYPE_ATOM + color = "purple" + datatype_flags = DATATYPE_FLAG_ALLOW_MANUAL_INPUT + +/datum/circuit_datatype/entity/convert_value(datum/port/port, value_to_convert) + var/atom/object = value_to_convert + if(QDELETED(object)) + return null + return object diff --git a/code/modules/wiremod/datatypes/number.dm b/code/modules/wiremod/datatypes/number.dm new file mode 100644 index 0000000000..7013340249 --- /dev/null +++ b/code/modules/wiremod/datatypes/number.dm @@ -0,0 +1,14 @@ +/datum/circuit_datatype/number + datatype = PORT_TYPE_NUMBER + color = "green" + datatype_flags = DATATYPE_FLAG_ALLOW_MANUAL_INPUT + +/datum/circuit_datatype/number/can_receive_from_datatype(datatype_to_check) + . = ..() + if(.) + return + + return datatype_to_check == PORT_TYPE_NUMBER + +/datum/circuit_datatype/number/handle_manual_input(datum/port/input/port, mob/user, user_input) + return text2num(user_input) diff --git a/code/modules/wiremod/datatypes/option.dm b/code/modules/wiremod/datatypes/option.dm new file mode 100644 index 0000000000..c6de346c9d --- /dev/null +++ b/code/modules/wiremod/datatypes/option.dm @@ -0,0 +1,33 @@ +/datum/port/input/option + var/list/possible_options + +/datum/port/input/option/New(obj/item/circuit_component/to_connect, name, datatype, trigger, default, possible_options) + . = ..() + src.possible_options = possible_options + set_value(default, force = TRUE) + +/datum/circuit_datatype/option + datatype = PORT_TYPE_OPTION + color = "violet" + datatype_flags = DATATYPE_FLAG_ALLOW_MANUAL_INPUT + +/datum/circuit_datatype/option/is_compatible(datum/port/gained_port) + return istype(gained_port, /datum/port/input/option) + +/datum/circuit_datatype/option/can_receive_from_datatype(datatype_to_check) + . = ..() + if(.) + return + + return datatype_to_check == PORT_TYPE_STRING + +/datum/circuit_datatype/option/convert_value(datum/port/input/option/port, value_to_convert) + if(!port.possible_options) + return null + + if(value_to_convert in port.possible_options) + return value_to_convert + return port.possible_options[1] + +/datum/circuit_datatype/option/datatype_ui_data(datum/port/input/option/port) + return port.possible_options diff --git a/code/modules/wiremod/datatypes/signal.dm b/code/modules/wiremod/datatypes/signal.dm new file mode 100644 index 0000000000..2226f509fc --- /dev/null +++ b/code/modules/wiremod/datatypes/signal.dm @@ -0,0 +1,17 @@ +/datum/circuit_datatype/signal + datatype = PORT_TYPE_SIGNAL + color = "teal" + datatype_flags = DATATYPE_FLAG_ALLOW_MANUAL_INPUT|DATATYPE_FLAG_AVOID_VALUE_UPDATE + +/datum/circuit_datatype/signal/can_receive_from_datatype(datatype_to_check) + . = ..() + if(.) + return + + return datatype_to_check == PORT_TYPE_NUMBER + +/datum/circuit_datatype/signal/handle_manual_input(datum/port/input/port, mob/user, user_input) + var/atom/parent = port.connected_component + if(parent) + parent.balloon_alert(user, "triggered [port.name]") + return COMPONENT_SIGNAL diff --git a/code/modules/wiremod/datatypes/string.dm b/code/modules/wiremod/datatypes/string.dm new file mode 100644 index 0000000000..99c61b4a29 --- /dev/null +++ b/code/modules/wiremod/datatypes/string.dm @@ -0,0 +1,17 @@ +/datum/circuit_datatype/string + datatype = PORT_TYPE_STRING + color = "orange" + datatype_flags = DATATYPE_FLAG_ALLOW_MANUAL_INPUT + +/datum/circuit_datatype/string/can_receive_from_datatype(datatype_to_check) + return TRUE + +/datum/circuit_datatype/string/convert_value(datum/port/port, value_to_convert) + if(isnull(value_to_convert)) + return + + // So that they can't easily get the name like this. + if(isatom(value_to_convert)) + return PORT_TYPE_ATOM + else + return copytext("[value_to_convert]", 1, PORT_MAX_STRING_LENGTH) diff --git a/code/modules/wiremod/preset/hello_world.dm b/code/modules/wiremod/preset/hello_world.dm new file mode 100644 index 0000000000..9aee88a300 --- /dev/null +++ b/code/modules/wiremod/preset/hello_world.dm @@ -0,0 +1,14 @@ +/** + * # Hello World preset + * + * Says "Hello World" when triggered. Needs to be wired up and connected first. + */ +/obj/item/integrated_circuit/loaded/hello_world + +/obj/item/integrated_circuit/loaded/hello_world/Initialize() + . = ..() + var/obj/item/circuit_component/speech/speech = new() + add_component(speech) + + speech.message.set_input("Hello World") + diff --git a/code/modules/wiremod/preset/speech_relay.dm b/code/modules/wiremod/preset/speech_relay.dm new file mode 100644 index 0000000000..5e5284bdeb --- /dev/null +++ b/code/modules/wiremod/preset/speech_relay.dm @@ -0,0 +1,20 @@ +/** + * # Speech Relay preset + * + * Acts like poly. Says whatever it hears. + */ +/obj/item/integrated_circuit/loaded/speech_relay + +/obj/item/integrated_circuit/loaded/speech_relay/Initialize() + . = ..() + var/obj/item/circuit_component/hear/hear = new() + add_component(hear) + hear.rel_x = 100 + hear.rel_y = 200 + + var/obj/item/circuit_component/speech/speech = new() + add_component(speech) + speech.rel_x = 400 + speech.rel_y = 200 + + speech.message.connect(hear.message_port) diff --git a/code/modules/wiremod/shell/airlock.dm b/code/modules/wiremod/shell/airlock.dm new file mode 100644 index 0000000000..b44bb40d02 --- /dev/null +++ b/code/modules/wiremod/shell/airlock.dm @@ -0,0 +1,133 @@ +/datum/wires/airlock/shell + holder_type = /obj/machinery/door/airlock/shell + proper_name = "Circuit Airlock" + +/datum/wires/airlock/shell/on_cut(wire, mend) + // Don't allow them to re-enable autoclose. + if(wire == WIRE_TIMING) + return + return ..() + +/obj/machinery/door/airlock/shell + name = "circuit airlock" + autoclose = FALSE + +/obj/machinery/door/airlock/shell/Initialize() + . = ..() + AddComponent( \ + /datum/component/shell, \ + unremovable_circuit_components = list(new /obj/item/circuit_component/airlock), \ + capacity = SHELL_CAPACITY_LARGE, \ + shell_flags = SHELL_FLAG_ALLOW_FAILURE_ACTION \ + ) + +/obj/machinery/door/airlock/shell/check_access(obj/item/I) + return FALSE + +/obj/machinery/door/airlock/shell/canAIControl(mob/user) + return FALSE + +/obj/machinery/door/airlock/shell/canAIHack(mob/user) + return FALSE + +/obj/machinery/door/airlock/shell/set_wires() + return new /datum/wires/airlock/shell(src) + +/obj/item/circuit_component/airlock + display_name = "Airlock" + desc = "The general interface with an airlock. Includes general statuses of the airlock" + + /// Called when attack_hand is called on the shell. + var/obj/machinery/door/airlock/attached_airlock + + /// Bolts the airlock (if possible) + var/datum/port/input/bolt + /// Unbolts the airlock (if possible) + var/datum/port/input/unbolt + /// Opens the airlock (if possible) + var/datum/port/input/open + /// Closes the airlock (if possible) + var/datum/port/input/close + + /// Contains whether the airlock is open or not + var/datum/port/output/is_open + /// Contains whether the airlock is bolted or not + var/datum/port/output/is_bolted + + /// Called when the airlock is opened. + var/datum/port/output/opened + /// Called when the airlock is closed + var/datum/port/output/closed + + /// Called when the airlock is bolted + var/datum/port/output/bolted + /// Called when the airlock is unbolted + var/datum/port/output/unbolted + +/obj/item/circuit_component/airlock/Initialize() + . = ..() + // Input Signals + bolt = add_input_port("Bolt", PORT_TYPE_SIGNAL) + unbolt = add_input_port("Unbolt", PORT_TYPE_SIGNAL) + open = add_input_port("Open", PORT_TYPE_SIGNAL) + close = add_input_port("Close", PORT_TYPE_SIGNAL) + // States + is_open = add_output_port("Is Open", PORT_TYPE_NUMBER) + is_bolted = add_output_port("Is Bolted", PORT_TYPE_NUMBER) + // Output Signals + opened = add_output_port("Opened", PORT_TYPE_SIGNAL) + closed = add_output_port("Closed", PORT_TYPE_SIGNAL) + bolted = add_output_port("Bolted", PORT_TYPE_SIGNAL) + unbolted = add_output_port("Unbolted", PORT_TYPE_SIGNAL) + +/obj/item/circuit_component/airlock/register_shell(atom/movable/shell) + . = ..() + if(istype(shell, /obj/machinery/door/airlock)) + attached_airlock = shell + RegisterSignal(shell, COMSIG_AIRLOCK_SET_BOLT, .proc/on_airlock_set_bolted) + RegisterSignal(shell, COMSIG_AIRLOCK_OPEN, .proc/on_airlock_open) + RegisterSignal(shell, COMSIG_AIRLOCK_CLOSE, .proc/on_airlock_closed) + +/obj/item/circuit_component/airlock/unregister_shell(atom/movable/shell) + attached_airlock = null + UnregisterSignal(shell, list( + COMSIG_AIRLOCK_SET_BOLT, + COMSIG_AIRLOCK_OPEN, + COMSIG_AIRLOCK_CLOSE, + )) + return ..() + +/obj/item/circuit_component/airlock/proc/on_airlock_set_bolted(datum/source, should_bolt) + SIGNAL_HANDLER + is_bolted.set_output(should_bolt) + if(should_bolt) + bolted.set_output(COMPONENT_SIGNAL) + else + unbolted.set_output(COMPONENT_SIGNAL) + +/obj/item/circuit_component/airlock/proc/on_airlock_open(datum/source, force) + SIGNAL_HANDLER + is_open.set_output(TRUE) + opened.set_output(COMPONENT_SIGNAL) + +/obj/item/circuit_component/airlock/proc/on_airlock_closed(datum/source, forced) + SIGNAL_HANDLER + is_open.set_output(FALSE) + closed.set_output(COMPONENT_SIGNAL) + +/obj/item/circuit_component/airlock/input_received(datum/port/input/port) + . = ..() + if(.) + return + + if(!attached_airlock) + return + + if(COMPONENT_TRIGGERED_BY(bolt, port)) + attached_airlock.bolt() + if(COMPONENT_TRIGGERED_BY(unbolt, port)) + attached_airlock.unbolt() + if(COMPONENT_TRIGGERED_BY(open, port) && attached_airlock.density) + INVOKE_ASYNC(attached_airlock, /obj/machinery/door/airlock.proc/open) + if(COMPONENT_TRIGGERED_BY(close, port) && !attached_airlock.density) + INVOKE_ASYNC(attached_airlock, /obj/machinery/door/airlock.proc/close) diff --git a/code/modules/wiremod/shell/bot.dm b/code/modules/wiremod/shell/bot.dm new file mode 100644 index 0000000000..f3c9fcae8d --- /dev/null +++ b/code/modules/wiremod/shell/bot.dm @@ -0,0 +1,55 @@ +/** + * # Bot + * + * Immobile (but not dense) shells that can interact with world. + */ +/obj/structure/bot + name = "bot" + icon = 'icons/obj/wiremod.dmi' + icon_state = "setup_medium_box" + + density = FALSE + light_system = MOVABLE_LIGHT + light_on = FALSE + +/obj/structure/bot/Initialize() + . = ..() + AddComponent( \ + /datum/component/shell, \ + unremovable_circuit_components = list(new /obj/item/circuit_component/bot), \ + capacity = SHELL_CAPACITY_LARGE, \ + shell_flags = SHELL_FLAG_USB_PORT, \ + ) + +/obj/item/circuit_component/bot + display_name = "Bot" + desc = "Triggers when someone interacts with the bot." + + /// Called when attack_hand is called on the shell. + var/datum/port/output/signal + /// The user who used the bot + var/datum/port/output/entity + +/obj/item/circuit_component/bot/Initialize() + . = ..() + entity = add_output_port("User", PORT_TYPE_ATOM) + signal = add_output_port("Signal", PORT_TYPE_SIGNAL) + +/obj/item/circuit_component/bot/Destroy() + signal = null + entity = null + return ..() + +/obj/item/circuit_component/bot/register_shell(atom/movable/shell) + RegisterSignal(shell, COMSIG_ATOM_ATTACK_HAND, .proc/on_attack_hand) + +/obj/item/circuit_component/bot/unregister_shell(atom/movable/shell) + UnregisterSignal(shell, COMSIG_ATOM_ATTACK_HAND) + +/obj/item/circuit_component/bot/proc/on_attack_hand(atom/source, mob/user) + SIGNAL_HANDLER + source.balloon_alert(user, "pushed button") + playsound(source, get_sfx("terminal_type"), 25, FALSE) + entity.set_output(user) + signal.set_output(COMPONENT_SIGNAL) + diff --git a/code/modules/wiremod/shell/brain_computer_interface.dm b/code/modules/wiremod/shell/brain_computer_interface.dm new file mode 100644 index 0000000000..d00166fb0c --- /dev/null +++ b/code/modules/wiremod/shell/brain_computer_interface.dm @@ -0,0 +1,553 @@ +/obj/item/organ/cyberimp/bci + name = "brain-computer interface" + desc = "An implant that can be placed in a user's head to control circuits using their brain." + icon = 'icons/obj/wiremod.dmi' + icon_state = "bci" + zone = BODY_ZONE_HEAD + w_class = WEIGHT_CLASS_TINY + +/obj/item/organ/cyberimp/bci/Initialize() + . = ..() + + AddComponent(/datum/component/shell, list( + new /obj/item/circuit_component/bci_core, + new /obj/item/circuit_component/bci_action(null, "One"), + new /obj/item/circuit_component/bci_action(null, "Two"), + new /obj/item/circuit_component/bci_action(null, "Three"), + ), SHELL_CAPACITY_SMALL) + +/obj/item/organ/cyberimp/bci/Insert(mob/living/carbon/reciever, special, drop_if_replaced) + . = ..() + + // Organs are put in nullspace, but this breaks circuit interactions + forceMove(reciever) + +/obj/item/organ/cyberimp/bci/say(message, bubble_type, list/spans, sanitize, datum/language/language, ignore_spam, forced) + if (owner) + // Otherwise say_dead will be called. + // It's intentional that a circuit for a dead person does not speak from the shell. + if (owner.stat == DEAD) + return + + owner.say(message, forced = "circuit speech") + else + return ..() + +/obj/item/circuit_component/bci_action + display_name = "BCI Action" + desc = "Represents an action the user can take when implanted with the brain-computer interface." + required_shells = list(/obj/item/organ/cyberimp/bci) + + /// The icon of the button + var/datum/port/input/option/icon_options + + /// The name to use for the button + var/datum/port/input/button_name + + /// Called when the user presses the button + var/datum/port/output/signal + + /// A reference to the action button itself + var/datum/action/innate/bci_action/bci_action + +/obj/item/circuit_component/bci_action/Initialize(mapload, default_icon) + . = ..() + + if (!isnull(default_icon)) + icon_options.set_input(default_icon) + + button_name = add_input_port("Name", PORT_TYPE_STRING) + + signal = add_output_port("Signal", PORT_TYPE_SIGNAL) + +/obj/item/circuit_component/bci_action/Destroy() + QDEL_NULL(bci_action) + return ..() + +/obj/item/circuit_component/bci_action/populate_options() + var/static/action_options = list( + "Blank", + + "One", + "Two", + "Three", + "Four", + "Five", + + "Blood", + "Bomb", + "Brain", + "Brain Damage", + "Cross", + "Electricity", + "Exclamation", + "Heart", + "Id", + "Info", + "Injection", + "Magnetism", + "Minus", + "Network", + "Plus", + "Power", + "Question", + "Radioactive", + "Reaction", + "Repair", + "Say", + "Scan", + "Shield", + "Skull", + "Sleep", + "Wireless", + ) + + icon_options = add_option_port("Icon", action_options) + +/obj/item/circuit_component/bci_action/register_shell(atom/movable/shell) + var/obj/item/organ/cyberimp/bci/bci = shell + + bci_action = new(src) + update_action() + + bci.actions += list(bci_action) + +/obj/item/circuit_component/bci_action/unregister_shell(atom/movable/shell) + var/obj/item/organ/cyberimp/bci/bci = shell + + bci.actions -= bci_action + QDEL_NULL(bci_action) + +/obj/item/circuit_component/bci_action/input_received(datum/port/input/port) + . = ..() + + if (.) + return + + if (!isnull(bci_action)) + update_action() + +/obj/item/circuit_component/bci_action/proc/update_action() + bci_action.name = button_name.value + bci_action.button_icon_state = "bci_[replacetextEx(lowertext(icon_options.value), " ", "_")]" + +/datum/action/innate/bci_action + name = "Action" + icon_icon = 'icons/mob/actions/actions_items.dmi' + check_flags = AB_CHECK_CONSCIOUS + button_icon_state = "bci_power" + + var/obj/item/circuit_component/bci_action/circuit_component + +/datum/action/innate/bci_action/New(obj/item/circuit_component/bci_action/circuit_component) + ..() + + src.circuit_component = circuit_component + +/datum/action/innate/bci_action/Destroy() + circuit_component.bci_action = null + circuit_component = null + + return ..() + +/datum/action/innate/bci_action/Activate() + circuit_component.signal.set_output(COMPONENT_SIGNAL) + +/obj/item/circuit_component/bci_core + display_name = "BCI Core" + desc = "Controls the core operations of the BCI." + + /// A reference to the action button to look at charge/get info + var/datum/action/innate/bci_charge_action/charge_action + + var/datum/port/input/message + var/datum/port/input/send_message_signal + + var/datum/port/output/user_port + + var/datum/weakref/user + +/obj/item/circuit_component/bci_core/Initialize() + . = ..() + + message = add_input_port("Message", PORT_TYPE_STRING) + send_message_signal = add_input_port("Send Message", PORT_TYPE_SIGNAL) + + user_port = add_output_port("User", PORT_TYPE_ATOM) + +/obj/item/circuit_component/bci_core/Destroy() + QDEL_NULL(charge_action) + return ..() + +/obj/item/circuit_component/bci_core/register_shell(atom/movable/shell) + var/obj/item/organ/cyberimp/bci/bci = shell + + charge_action = new(src) + bci.actions += list(charge_action) + + RegisterSignal(shell, COMSIG_ORGAN_IMPLANTED, .proc/on_organ_implanted) + RegisterSignal(shell, COMSIG_ORGAN_REMOVED, .proc/on_organ_removed) + +/obj/item/circuit_component/bci_core/unregister_shell(atom/movable/shell) + var/obj/item/organ/cyberimp/bci/bci = shell + + bci.actions -= charge_action + QDEL_NULL(charge_action) + + UnregisterSignal(shell, list( + COMSIG_ORGAN_IMPLANTED, + COMSIG_ORGAN_REMOVED, + )) + +/obj/item/circuit_component/bci_core/input_received(datum/port/input/port) + . = ..() + if (.) + return . + + if (COMPONENT_TRIGGERED_BY(send_message_signal, port)) + var/sent_message = trim(message.value) + if (!sent_message) + return + + var/mob/living/carbon/resolved_owner = user?.resolve() + if (isnull(resolved_owner)) + return + + if (resolved_owner.stat == DEAD) + return + + to_chat(resolved_owner, "You hear a strange, robotic voice in your head... \"[span_robot("[html_encode(sent_message)]")]\"") + +/obj/item/circuit_component/bci_core/proc/on_organ_implanted(datum/source, mob/living/carbon/owner) + SIGNAL_HANDLER + + user_port.set_output(owner) + user = WEAKREF(owner) + + RegisterSignal(owner, COMSIG_PARENT_EXAMINE, .proc/on_examine) + RegisterSignal(owner, COMSIG_PROCESS_BORGCHARGER_OCCUPANT, .proc/on_borg_charge) + RegisterSignal(owner, COMSIG_LIVING_ELECTROCUTE_ACT, .proc/on_electrocute) + +/obj/item/circuit_component/bci_core/proc/on_organ_removed(datum/source, mob/living/carbon/owner) + SIGNAL_HANDLER + + user_port.set_output(null) + user = null + + UnregisterSignal(owner, list( + COMSIG_PARENT_EXAMINE, + COMSIG_PROCESS_BORGCHARGER_OCCUPANT, + COMSIG_LIVING_ELECTROCUTE_ACT, + )) + +/obj/item/circuit_component/bci_core/proc/on_borg_charge(datum/source, amount) + SIGNAL_HANDLER + + if (isnull(parent.cell)) + return + + parent.cell.give(amount) + +/obj/item/circuit_component/bci_core/proc/on_electrocute(datum/source, shock_damage, siemens_coefficient, flags) + SIGNAL_HANDLER + + if (isnull(parent.cell)) + return + + if (flags & SHOCK_ILLUSION) + return + + parent.cell.give(shock_damage * 2) + to_chat(source, span_notice("You absorb some of the shock into your [parent.name]!")) + +/obj/item/circuit_component/bci_core/proc/on_examine(datum/source, mob/mob, list/examine_text) + SIGNAL_HANDLER + + if (isobserver(mob)) + examine_text += span_notice("[source.p_they(capitalized = TRUE)] [source.p_have()] \a [parent] implanted in [source.p_them()].") + +/obj/item/circuit_component/bci_core/Topic(href, list/href_list) + ..() + + if (!isobserver(usr)) + return + + if (href_list["open_bci"]) + parent.attack_ghost(usr) + +/datum/action/innate/bci_charge_action + name = "Check BCI Charge" + check_flags = NONE + icon_icon = 'icons/obj/power.dmi' + button_icon_state = "cell" + + var/obj/item/circuit_component/bci_core/circuit_component + +/datum/action/innate/bci_charge_action/New(obj/item/circuit_component/bci_core/circuit_component) + ..() + + src.circuit_component = circuit_component + + button.maptext_x = 2 + button.maptext_y = 0 + update_maptext() + + START_PROCESSING(SSobj, src) + +/datum/action/innate/bci_charge_action/Destroy() + circuit_component.charge_action = null + circuit_component = null + + STOP_PROCESSING(SSobj, src) + + return ..() + +/datum/action/innate/bci_charge_action/Trigger() + var/obj/item/stock_parts/cell/cell = circuit_component.parent.cell + + if (isnull(cell)) + to_chat(owner, span_boldwarning("[circuit_component.parent] has no power cell.")) + else + to_chat(owner, span_info("[circuit_component.parent]'s [cell.name] has [cell.percent()]% charge left.")) + to_chat(owner, span_info("You can recharge it by using a cyborg recharging station.")) + +/datum/action/innate/bci_charge_action/process(delta_time) + update_maptext() + +/datum/action/innate/bci_charge_action/proc/update_maptext() + var/obj/item/stock_parts/cell/cell = circuit_component.parent.cell + button.maptext = cell ? MAPTEXT("[cell.percent()]%") : "" + +/obj/machinery/bci_implanter + name = "brain-computer interface manipulation chamber" + desc = "A machine that, when given a brain-computer interface, will implant it into an occupant. Otherwise, will remove any brain-computer interfaces they already have." + circuit = /obj/item/circuitboard/machine/bci_implanter + icon = 'icons/obj/machines/bci_implanter.dmi' + icon_state = "bci_implanter" + base_icon_state = "bci_implanter" + layer = ABOVE_WINDOW_LAYER + use_power = IDLE_POWER_USE + anchored = TRUE + density = TRUE + obj_flags = NO_BUILD // Becomes undense when the door is open + idle_power_usage = 50 + active_power_usage = 300 + + var/busy = FALSE + var/busy_icon_state + var/locked = FALSE + + var/datum/weakref/bci_to_implant + + COOLDOWN_DECLARE(message_cooldown) + +/obj/machinery/bci_implanter/Initialize() + . = ..() + occupant_typecache = typecacheof(/mob/living/carbon) + +/obj/machinery/bci_implanter/Destroy() + QDEL_NULL(bci_to_implant) + return ..() + +/obj/machinery/bci_implanter/examine(mob/user) + . = ..() + + if (isnull(bci_to_implant?.resolve())) + . += span_notice("There is no BCI inserted.") + else + . += span_notice("Right-click to remove current BCI.") + +/obj/machinery/bci_implanter/proc/set_busy(status, working_icon) + busy = status + busy_icon_state = working_icon + update_appearance() + +/obj/machinery/bci_implanter/update_icon_state() + if (occupant) + icon_state = busy ? busy_icon_state : "[base_icon_state]_occupied" + return ..() + icon_state = "[base_icon_state][state_open ? "_open" : null]" + return ..() + +/obj/machinery/bci_implanter/update_overlays() + var/list/overlays = ..() + + if ((machine_stat & MAINT) || panel_open) + overlays += "maint" + return overlays + + if (machine_stat & (NOPOWER|BROKEN)) + return overlays + + if (busy || locked) + overlays += "red" + if (locked) + overlays += "bolted" + return overlays + + overlays += "green" + + return overlays + +/obj/machinery/bci_implanter/attack_hand_secondary(mob/user, list/modifiers) + . = ..() + if (. == SECONDARY_ATTACK_CANCEL_ATTACK_CHAIN) + return . + + if(!user.Adjacent(src)) + return + + if (locked) + balloon_alert(user, "it's locked!") + return SECONDARY_ATTACK_CANCEL_ATTACK_CHAIN + + var/obj/item/organ/cyberimp/bci/bci_to_implant_resolved = bci_to_implant?.resolve() + if (isnull(bci_to_implant_resolved)) + balloon_alert(user, "no bci inserted!") + else + user.put_in_hands(bci_to_implant_resolved) + balloon_alert(user, "ejected bci") + + bci_to_implant = null + + return SECONDARY_ATTACK_CANCEL_ATTACK_CHAIN + +/obj/machinery/bci_implanter/attackby(obj/item/weapon, mob/user, params) + var/obj/item/organ/cyberimp/bci/new_bci = weapon + if (istype(new_bci)) + if (!(locate(/obj/item/integrated_circuit) in new_bci)) + balloon_alert(user, "bci has no circuit!") + return + + var/obj/item/organ/cyberimp/bci/previous_bci_to_implant = bci_to_implant?.resolve() + + bci_to_implant = WEAKREF(weapon) + weapon.forceMove(src) + + if (isnull(previous_bci_to_implant)) + balloon_alert(user, "inserted bci") + else + balloon_alert(user, "swapped bci") + user.put_in_hands(previous_bci_to_implant) + + return + + return ..() + +/obj/machinery/bci_implanter/attackby_secondary(obj/item/weapon, mob/user, params) + if (!occupant && default_deconstruction_screwdriver(user, icon_state, icon_state, weapon)) + update_appearance() + return SECONDARY_ATTACK_CANCEL_ATTACK_CHAIN + + if (default_pry_open(weapon)) + return SECONDARY_ATTACK_CANCEL_ATTACK_CHAIN + + if (default_deconstruction_crowbar(weapon)) + return SECONDARY_ATTACK_CANCEL_ATTACK_CHAIN + + return SECONDARY_ATTACK_CANCEL_ATTACK_CHAIN + +/obj/machinery/bci_implanter/proc/start_process() + if (machine_stat & (NOPOWER|BROKEN)) + return + if ((machine_stat & MAINT) || panel_open) + return + if (!occupant || busy) + return + + var/locked_state = locked + locked = TRUE + + set_busy(TRUE, "[initial(icon_state)]_raising") + addtimer(CALLBACK(src, .proc/set_busy, TRUE, "[initial(icon_state)]_active"), 1 SECONDS) + addtimer(CALLBACK(src, .proc/set_busy, TRUE, "[initial(icon_state)]_falling"), 2 SECONDS) + addtimer(CALLBACK(src, .proc/complete_process, locked_state), 3 SECONDS) + +/obj/machinery/bci_implanter/proc/complete_process(locked_state) + locked = locked_state + set_busy(FALSE) + + var/mob/living/carbon/carbon_occupant = occupant + if (!istype(carbon_occupant)) + return + + playsound(loc, 'sound/machines/ping.ogg', 30, FALSE) + + var/obj/item/organ/cyberimp/bci/bci_organ = carbon_occupant.getorgan(/obj/item/organ/cyberimp/bci) + var/obj/item/organ/cyberimp/bci/bci_to_implant_resolved = bci_to_implant?.resolve() + + if (bci_organ) + bci_organ.Remove(carbon_occupant) + + if (isnull(bci_to_implant_resolved)) + say("Occupant's previous brain-computer interface has been transferred to internal storage unit.") + bci_organ.forceMove(src) + bci_to_implant = WEAKREF(bci_organ) + else + say("Occupant's previous brain-computer interface has been ejected.") + bci_organ.forceMove(drop_location()) + else if (!isnull(bci_to_implant_resolved)) + say("Occupant has been injected with [bci_to_implant_resolved].") + bci_to_implant_resolved.Insert(carbon_occupant) + bci_to_implant = null + +/obj/machinery/bci_implanter/open_machine() + if(state_open) + return FALSE + + ..() + + return TRUE + +/obj/machinery/bci_implanter/close_machine(mob/living/carbon/user) + if(!state_open) + return FALSE + + ..() + + var/mob/living/carbon/carbon_occupant = occupant + if (istype(occupant)) + var/obj/item/organ/cyberimp/bci/existing_bci_organ = carbon_occupant.getorgan(/obj/item/organ/cyberimp/bci) + if (isnull(existing_bci_organ) && isnull(bci_to_implant?.resolve())) + say("No brain-computer interface inserted, and occupant does not have one. Insert a BCI to implant one.") + playsound(src, 'sound/machines/buzz-sigh.ogg', 30, TRUE) + return FALSE + + addtimer(CALLBACK(src, .proc/start_process), 1 SECONDS) + return TRUE + +/obj/machinery/bci_implanter/relaymove(mob/living/user, direction) + var/message + + if (locked) + message = "it won't budge!" + else if (user.stat != CONSCIOUS) + message = "you don't have the energy!" + + if (!isnull(message)) + if (COOLDOWN_FINISHED(src, message_cooldown)) + COOLDOWN_START(src, message_cooldown, 5 SECONDS) + balloon_alert(user, message) + + return + + open_machine() + +/obj/machinery/bci_implanter/interact(mob/user) + if (state_open) + close_machine(null, user) + return + else if (locked) + balloon_alert(user, "it's locked!") + return + + open_machine() + +/obj/item/circuitboard/machine/bci_implanter + name = "Brain-Computer Interface Manipulation Chamber (Machine Board)" + greyscale_colors = CIRCUIT_COLOR_SCIENCE + build_path = /obj/machinery/bci_implanter + req_components = list( + /obj/item/stock_parts/micro_laser = 2, + /obj/item/stock_parts/manipulator = 1, + ) diff --git a/code/modules/wiremod/shell/compact_remote.dm b/code/modules/wiremod/shell/compact_remote.dm new file mode 100644 index 0000000000..d5fe33e285 --- /dev/null +++ b/code/modules/wiremod/shell/compact_remote.dm @@ -0,0 +1,51 @@ +/** + * # Compact Remote + * + * A handheld device with one big button. + */ +/obj/item/compact_remote + name = "compact remote" + icon = 'icons/obj/wiremod.dmi' + icon_state = "setup_small_simple" + inhand_icon_state = "electronic" + worn_icon_state = "electronic" + lefthand_file = 'icons/mob/inhands/misc/devices_lefthand.dmi' + righthand_file = 'icons/mob/inhands/misc/devices_righthand.dmi' + light_system = MOVABLE_LIGHT_DIRECTIONAL + light_on = FALSE + +/obj/item/compact_remote/Initialize() + . = ..() + AddComponent(/datum/component/shell, list( + new /obj/item/circuit_component/compact_remote() + ), SHELL_CAPACITY_SMALL) + +/obj/item/circuit_component/compact_remote + display_name = "Compact Remote" + desc = "Used to receive inputs from the compact remote shell. Use the shell in hand to trigger the output signal." + + /// Called when attack_self is called on the shell. + var/datum/port/output/signal + /// The user who used the bot + var/datum/port/output/entity + +/obj/item/circuit_component/compact_remote/Initialize() + . = ..() + entity = add_output_port("User", PORT_TYPE_ATOM) + signal = add_output_port("Signal", PORT_TYPE_SIGNAL) + +/obj/item/circuit_component/compact_remote/register_shell(atom/movable/shell) + RegisterSignal(shell, COMSIG_ITEM_ATTACK_SELF, .proc/send_trigger) + +/obj/item/circuit_component/compact_remote/unregister_shell(atom/movable/shell) + UnregisterSignal(shell, COMSIG_ITEM_ATTACK_SELF) + +/** + * Called when the shell item is used in hand. + */ +/obj/item/circuit_component/compact_remote/proc/send_trigger(atom/source, mob/user) + SIGNAL_HANDLER + source.balloon_alert(user, "clicked primary button") + playsound(source, get_sfx("terminal_type"), 25, FALSE) + entity.set_output(user) + signal.set_output(COMPONENT_SIGNAL) diff --git a/code/modules/wiremod/shell/controller.dm b/code/modules/wiremod/shell/controller.dm new file mode 100644 index 0000000000..d3af0ca88e --- /dev/null +++ b/code/modules/wiremod/shell/controller.dm @@ -0,0 +1,89 @@ +/** + * # Compact Remote + * + * A handheld device with several buttons. + * In game, this translates to having different signals for normal usage, alt-clicking, and ctrl-clicking when in your hand. + */ +/obj/item/controller + name = "controller" + icon = 'icons/obj/wiremod.dmi' + icon_state = "setup_small_calc" + inhand_icon_state = "electronic" + worn_icon_state = "electronic" + lefthand_file = 'icons/mob/inhands/misc/devices_lefthand.dmi' + righthand_file = 'icons/mob/inhands/misc/devices_righthand.dmi' + light_system = MOVABLE_LIGHT_DIRECTIONAL + light_on = FALSE + +/obj/item/controller/Initialize() + . = ..() + AddComponent(/datum/component/shell, list( + new /obj/item/circuit_component/controller() + ), SHELL_CAPACITY_MEDIUM) + +/obj/item/circuit_component/controller + display_name = "Controller" + desc = "Used to receive inputs from the controller shell. Use the shell in hand to trigger the output signal. Alt-click for the alternate signal. Right click for the extra signal." + + /// The three separate buttons that are called in attack_hand on the shell. + var/datum/port/output/signal + var/datum/port/output/alt + var/datum/port/output/right + + /// The entity output + var/datum/port/output/entity + +/obj/item/circuit_component/controller/Initialize() + . = ..() + entity = add_output_port("User", PORT_TYPE_ATOM) + signal = add_output_port("Signal", PORT_TYPE_SIGNAL) + alt = add_output_port("Alternate Signal", PORT_TYPE_SIGNAL) + right = add_output_port("Extra Signal", PORT_TYPE_SIGNAL) + +/obj/item/circuit_component/controller/register_shell(atom/movable/shell) + RegisterSignal(shell, COMSIG_ITEM_ATTACK_SELF, .proc/send_trigger) + RegisterSignal(shell, COMSIG_CLICK_ALT, .proc/send_alternate_signal) + RegisterSignal(shell, COMSIG_ITEM_ATTACK_SELF_SECONDARY, .proc/send_right_signal) + +/obj/item/circuit_component/controller/unregister_shell(atom/movable/shell) + UnregisterSignal(shell, list( + COMSIG_ITEM_ATTACK_SELF, + COMSIG_ITEM_ATTACK_SELF_SECONDARY, + COMSIG_CLICK_ALT, + )) + +/** + * Called when the shell item is used in hand + */ +/obj/item/circuit_component/controller/proc/send_trigger(atom/source, mob/user) + SIGNAL_HANDLER + if(!user.Adjacent(source)) + return + source.balloon_alert(user, "clicked primary button") + playsound(source, get_sfx("terminal_type"), 25, FALSE) + entity.set_output(user) + signal.set_output(COMPONENT_SIGNAL) + +/** + * Called when the shell item is alt-clicked + */ +/obj/item/circuit_component/controller/proc/send_alternate_signal(atom/source, mob/user) + SIGNAL_HANDLER + if(!user.Adjacent(source)) + return + source.balloon_alert(user, "clicked alternate button") + playsound(source, get_sfx("terminal_type"), 25, FALSE) + entity.set_output(user) + alt.set_output(COMPONENT_SIGNAL) + +/** + * Called when the shell item is right-clicked in active hand + */ +/obj/item/circuit_component/controller/proc/send_right_signal(atom/source, mob/user) + SIGNAL_HANDLER + if(!user.Adjacent(source)) + return + source.balloon_alert(user, "clicked extra button") + playsound(source, get_sfx("terminal_type"), 25, FALSE) + entity.set_output(user) + right.set_output(COMPONENT_SIGNAL) diff --git a/code/modules/wiremod/shell/drone.dm b/code/modules/wiremod/shell/drone.dm new file mode 100644 index 0000000000..d091f20b65 --- /dev/null +++ b/code/modules/wiremod/shell/drone.dm @@ -0,0 +1,82 @@ +/** + * # Drone + * + * A movable mob that can be fed inputs on which direction to travel. + */ +/mob/living/circuit_drone + name = "drone" + icon = 'icons/obj/wiremod.dmi' + icon_state = "setup_medium_med" + living_flags = 0 + light_system = MOVABLE_LIGHT_DIRECTIONAL + light_on = FALSE + +/mob/living/circuit_drone/Initialize() + . = ..() + AddComponent(/datum/component/shell, list( + new /obj/item/circuit_component/bot_circuit() + ), SHELL_CAPACITY_LARGE) + +/mob/living/circuit_drone/updatehealth() + . = ..() + if(health < 0) + gib(no_brain = TRUE, no_organs = TRUE, no_bodyparts = TRUE) + +/mob/living/circuit_drone/spawn_gibs() + new /obj/effect/gibspawner/robot(drop_location(), src, get_static_viruses()) + +/obj/item/circuit_component/bot_circuit + display_name = "Drone" + desc = "Used to send movement output signals to the drone shell." + + /// The inputs to allow for the drone to move + var/datum/port/input/north + var/datum/port/input/east + var/datum/port/input/south + var/datum/port/input/west + + // Done like this so that travelling diagonally is more simple + COOLDOWN_DECLARE(north_delay) + COOLDOWN_DECLARE(east_delay) + COOLDOWN_DECLARE(south_delay) + COOLDOWN_DECLARE(west_delay) + + /// Delay between each movement + var/move_delay = 0.2 SECONDS + +/obj/item/circuit_component/bot_circuit/Initialize() + . = ..() + north = add_input_port("Move North", PORT_TYPE_SIGNAL) + east = add_input_port("Move East", PORT_TYPE_SIGNAL) + south = add_input_port("Move South", PORT_TYPE_SIGNAL) + west = add_input_port("Move West", PORT_TYPE_SIGNAL) + +/obj/item/circuit_component/bot_circuit/input_received(datum/port/input/port) + . = ..() + if(.) + return + + var/mob/living/shell = parent.shell + if(!istype(shell) || shell.stat) + return + + var/direction + + if(COMPONENT_TRIGGERED_BY(north, port) && COOLDOWN_FINISHED(src, north_delay)) + direction = NORTH + COOLDOWN_START(src, north_delay, move_delay) + else if(COMPONENT_TRIGGERED_BY(east, port) && COOLDOWN_FINISHED(src, east_delay)) + direction = EAST + COOLDOWN_START(src, east_delay, move_delay) + else if(COMPONENT_TRIGGERED_BY(south, port) && COOLDOWN_FINISHED(src, south_delay)) + direction = SOUTH + COOLDOWN_START(src, south_delay, move_delay) + else if(COMPONENT_TRIGGERED_BY(west, port) && COOLDOWN_FINISHED(src, west_delay)) + direction = WEST + COOLDOWN_START(src, west_delay, move_delay) + + if(!direction) + return + + if(shell.Process_Spacemove(direction)) + shell.Move(get_step(shell, direction), direction) diff --git a/code/modules/wiremod/shell/moneybot.dm b/code/modules/wiremod/shell/moneybot.dm new file mode 100644 index 0000000000..fe3cd30733 --- /dev/null +++ b/code/modules/wiremod/shell/moneybot.dm @@ -0,0 +1,140 @@ +/** + * # Money Bot + * + * Immobile (but not dense) shell that can receive and dispense money. + */ +/obj/structure/money_bot + name = "money bot" + icon = 'icons/obj/wiremod.dmi' + icon_state = "setup_large" + + density = FALSE + light_system = MOVABLE_LIGHT + light_on = FALSE + + var/stored_money = 0 + +/obj/structure/money_bot/deconstruct(disassembled) + new /obj/item/holochip(drop_location(), stored_money) + return ..() + +/obj/structure/money_bot/proc/add_money(to_add) + stored_money += to_add + SEND_SIGNAL(src, COMSIG_MONEYBOT_ADD_MONEY, to_add) + +/obj/structure/money_bot/Initialize() + . = ..() + AddComponent(/datum/component/shell, list( + new /obj/item/circuit_component/money_bot(), + new /obj/item/circuit_component/money_dispenser() + ), SHELL_CAPACITY_LARGE) + +/obj/structure/money_bot/wrench_act(mob/living/user, obj/item/tool) + set_anchored(!anchored) + tool.play_tool_sound(src) + balloon_alert(user, "You [anchored?"secure":"unsecure"] [src].") + return TRUE + + +/obj/item/circuit_component/money_dispenser + display_name = "Money Dispenser" + desc = "Used to dispense money from the money bot. Money is taken from the internal storage of money." + circuit_flags = CIRCUIT_FLAG_INPUT_SIGNAL|CIRCUIT_FLAG_OUTPUT_SIGNAL + + /// The amount of money to dispense + var/datum/port/input/dispense_amount + + /// Outputs a signal when it fails to output any money. + var/datum/port/output/on_fail + + var/obj/structure/money_bot/attached_bot + +/obj/item/circuit_component/money_dispenser/Initialize() + . = ..() + dispense_amount = add_input_port("Amount", PORT_TYPE_NUMBER) + on_fail = add_output_port("On Failed", PORT_TYPE_SIGNAL) + +/obj/item/circuit_component/money_dispenser/register_shell(atom/movable/shell) + . = ..() + if(istype(shell, /obj/structure/money_bot)) + attached_bot = shell + +/obj/item/circuit_component/money_dispenser/unregister_shell(atom/movable/shell) + attached_bot = null + return ..() + +/obj/item/circuit_component/money_dispenser/input_received(datum/port/input/port) + . = ..() + if(.) + return + + if(!attached_bot) + return + + var/to_dispense = clamp(dispense_amount.value, 0, attached_bot.stored_money) + if(!to_dispense) + on_fail.set_output(COMPONENT_SIGNAL) + return + + attached_bot.add_money(-to_dispense) + new /obj/item/holochip(drop_location(), to_dispense) + +/obj/item/circuit_component/money_bot + display_name = "Money Bot" + var/obj/structure/money_bot/attached_bot + desc = "Used to receive input signals when money is inserted into the money bot shell and also keep track of the total money in the shell." + + /// Total money in the shell + var/datum/port/output/total_money + /// Amount of the last money inputted into the shell + var/datum/port/output/money_input + /// Trigger for when money is inputted into the shell + var/datum/port/output/money_trigger + /// The person who input the money + var/datum/port/output/entity + +/obj/item/circuit_component/money_bot/Initialize() + . = ..() + total_money = add_output_port("Total Money", PORT_TYPE_NUMBER) + money_input = add_output_port("Last Input Money", PORT_TYPE_NUMBER) + entity = add_output_port("User", PORT_TYPE_ATOM) + money_trigger = add_output_port("Money Input", PORT_TYPE_SIGNAL) + +/obj/item/circuit_component/money_bot/register_shell(atom/movable/shell) + . = ..() + if(istype(shell, /obj/structure/money_bot)) + attached_bot = shell + total_money.set_output(attached_bot.stored_money) + RegisterSignal(shell, COMSIG_PARENT_ATTACKBY, .proc/handle_money_insert) + RegisterSignal(shell, COMSIG_MONEYBOT_ADD_MONEY, .proc/handle_money_update) + +/obj/item/circuit_component/money_bot/unregister_shell(atom/movable/shell) + UnregisterSignal(shell, list( + COMSIG_PARENT_ATTACKBY, + COMSIG_MONEYBOT_ADD_MONEY, + )) + total_money.set_output(null) + attached_bot = null + return ..() + +/obj/item/circuit_component/money_bot/proc/handle_money_insert(atom/source, obj/item/item, mob/living/attacker) + SIGNAL_HANDLER + if(!attached_bot || !iscash(item)) + return + + var/amount_to_insert = item.get_item_credit_value() + if(!amount_to_insert) + balloon_alert(attacker, "this has no value!") + return + + attached_bot.add_money(amount_to_insert) + balloon_alert(attacker, "inserted [amount_to_insert] credits.") + money_input.set_output(amount_to_insert) + entity.set_output(attacker) + money_trigger.set_output(COMPONENT_SIGNAL) + qdel(item) + +/obj/item/circuit_component/money_bot/proc/handle_money_update(atom/source) + SIGNAL_HANDLER + if(attached_bot) + total_money.set_output(attached_bot.stored_money) diff --git a/code/modules/wiremod/shell/scanner.dm b/code/modules/wiremod/shell/scanner.dm new file mode 100644 index 0000000000..fd46910914 --- /dev/null +++ b/code/modules/wiremod/shell/scanner.dm @@ -0,0 +1,62 @@ +/** + * # Scanner + * + * A handheld device that lets you flash it over people. + */ +/obj/item/wiremod_scanner + name = "scanner" + icon = 'icons/obj/wiremod.dmi' + icon_state = "setup_small" + inhand_icon_state = "electronic" + worn_icon_state = "electronic" + lefthand_file = 'icons/mob/inhands/misc/devices_lefthand.dmi' + righthand_file = 'icons/mob/inhands/misc/devices_righthand.dmi' + light_system = MOVABLE_LIGHT_DIRECTIONAL + light_on = FALSE + +/obj/item/wiremod_scanner/Initialize() + . = ..() + AddComponent(/datum/component/shell, list( + new /obj/item/circuit_component/wiremod_scanner() + ), SHELL_CAPACITY_SMALL) + +/obj/item/circuit_component/wiremod_scanner + display_name = "Scanner" + desc = "Used to receive scanned entities from the scanner." + + /// Called when afterattack is called on the shell. + var/datum/port/output/signal + + /// The attacker + var/datum/port/output/attacker + + /// The entity being attacked + var/datum/port/output/attacking + + + +/obj/item/circuit_component/wiremod_scanner/Initialize() + . = ..() + attacker = add_output_port("Scanner", PORT_TYPE_ATOM) + attacking = add_output_port("Scanned Entity", PORT_TYPE_ATOM) + signal = add_output_port("Scanned", PORT_TYPE_SIGNAL) + +/obj/item/circuit_component/wiremod_scanner/register_shell(atom/movable/shell) + RegisterSignal(shell, COMSIG_ITEM_AFTERATTACK, .proc/handle_afterattack) + +/obj/item/circuit_component/wiremod_scanner/unregister_shell(atom/movable/shell) + UnregisterSignal(shell, COMSIG_ITEM_AFTERATTACK) + +/** + * Called when the shell item attacks something + */ +/obj/item/circuit_component/wiremod_scanner/proc/handle_afterattack(atom/source, atom/target, mob/user, proximity_flag) + SIGNAL_HANDLER + if(!proximity_flag) + return + source.balloon_alert(user, "scanned object") + playsound(source, get_sfx("terminal_type"), 25, FALSE) + attacker.set_output(user) + attacking.set_output(target) + signal.set_output(COMPONENT_SIGNAL) + diff --git a/code/modules/wiremod/shell/scanner_gate.dm b/code/modules/wiremod/shell/scanner_gate.dm new file mode 100644 index 0000000000..312c690bd8 --- /dev/null +++ b/code/modules/wiremod/shell/scanner_gate.dm @@ -0,0 +1,66 @@ +/obj/structure/scanner_gate_shell + name = "circuit scanner gate" + desc = "A gate able to perform mid-depth scans on any organisms who pass under it." + icon = 'icons/obj/machines/scangate.dmi' + icon_state = "scangate_black" + var/scanline_timer + +/obj/structure/scanner_gate_shell/Initialize() + . = ..() + set_scanline("passive") + var/static/list/loc_connections = list( + COMSIG_ATOM_ENTERED = .proc/on_entered, + ) + AddElement(/datum/element/connect_loc, loc_connections) + + AddComponent(/datum/component/shell, list( + new /obj/item/circuit_component/scanner_gate() + ), SHELL_CAPACITY_LARGE, SHELL_FLAG_REQUIRE_ANCHOR) + +/obj/structure/scanner_gate_shell/wrench_act(mob/living/user, obj/item/tool) + set_anchored(!anchored) + tool.play_tool_sound(src) + balloon_alert(user, "You [anchored?"secure":"unsecure"] [src].") + return TRUE + +/obj/structure/scanner_gate_shell/proc/on_entered(datum/source, atom/movable/AM) + SIGNAL_HANDLER + set_scanline("scanning", 10) + SEND_SIGNAL(src, COMSIG_SCANGATE_SHELL_PASS, AM) + +/obj/structure/scanner_gate_shell/proc/set_scanline(type, duration) + cut_overlays() + deltimer(scanline_timer) + add_overlay(type) + if(duration) + scanline_timer = addtimer(CALLBACK(src, .proc/set_scanline, "passive"), duration, TIMER_STOPPABLE) + +/obj/item/circuit_component/scanner_gate + display_name = "Scanner Gate" + desc = "A gate able to perform mid-depth scans on any object that pass through it." + + circuit_flags = CIRCUIT_FLAG_OUTPUT_SIGNAL + + var/datum/port/output/scanned + + var/obj/structure/scanner_gate_shell/attached_gate + +/obj/item/circuit_component/scanner_gate/Initialize() + . = ..() + scanned = add_output_port("Scanned Object", PORT_TYPE_ATOM) + +/obj/item/circuit_component/scanner_gate/register_shell(atom/movable/shell) + . = ..() + if(istype(shell, /obj/structure/scanner_gate_shell)) + attached_gate = shell + RegisterSignal(attached_gate, COMSIG_SCANGATE_SHELL_PASS, .proc/on_trigger) + +/obj/item/circuit_component/scanner_gate/unregister_shell(atom/movable/shell) + UnregisterSignal(attached_gate, COMSIG_SCANGATE_SHELL_PASS) + attached_gate = null + return ..() + +/obj/item/circuit_component/scanner_gate/proc/on_trigger(datum/source, atom/movable/passed) + SIGNAL_HANDLER + scanned.set_output(passed) + trigger_output.set_output(COMPONENT_SIGNAL) diff --git a/code/modules/wiremod/shell/server.dm b/code/modules/wiremod/shell/server.dm new file mode 100644 index 0000000000..5e5a44f897 --- /dev/null +++ b/code/modules/wiremod/shell/server.dm @@ -0,0 +1,24 @@ +/** + * # Server + * + * Immobile (but not dense) shells that can interact with + * world. + */ +/obj/structure/server + name = "server" + icon = 'icons/obj/wiremod.dmi' + icon_state = "setup_stationary" + + density = TRUE + light_system = MOVABLE_LIGHT + light_on = FALSE + +/obj/structure/server/Initialize() + . = ..() + AddComponent(/datum/component/shell, null, SHELL_CAPACITY_VERY_LARGE, SHELL_FLAG_REQUIRE_ANCHOR|SHELL_FLAG_USB_PORT) + +/obj/structure/server/wrench_act(mob/living/user, obj/item/tool) + set_anchored(!anchored) + tool.play_tool_sound(src) + balloon_alert(user, "You [anchored?"secure":"unsecure"] [src].") + return TRUE diff --git a/code/modules/wiremod/shell/shell_items.dm b/code/modules/wiremod/shell/shell_items.dm new file mode 100644 index 0000000000..4e6673e7c8 --- /dev/null +++ b/code/modules/wiremod/shell/shell_items.dm @@ -0,0 +1,65 @@ +/** + * # Shell Item + * + * Printed out by protolathes. Screwdriver to complete the shell. + */ +/obj/item/shell + name = "assembly" + desc = "A shell assembly that can be completed by screwdrivering it." + icon = 'icons/obj/wiremod.dmi' + var/shell_to_spawn + var/screw_delay = 3 SECONDS + +/obj/item/shell/screwdriver_act(mob/living/user, obj/item/tool) + user.visible_message(span_notice("[user] begins finishing [src]."), span_notice("You begin finishing [src].")) + tool.play_tool_sound(src) + if(!do_after(user, screw_delay, src)) + return + user.visible_message(span_notice("[user] finishes [src]."), span_notice("You finish [src].")) + + var/turf/drop_loc = drop_location() + + qdel(src) + if(drop_loc) + new shell_to_spawn(drop_loc) + + return TRUE + +/obj/item/shell/bot + name = "bot assembly" + icon_state = "setup_medium_box-open" + shell_to_spawn = /obj/structure/bot + +/obj/item/shell/money_bot + name = "money bot assembly" + icon_state = "setup_large-open" + shell_to_spawn = /obj/structure/money_bot + +/obj/item/shell/drone + name = "drone assembly" + icon_state = "setup_medium_med-open" + shell_to_spawn = /mob/living/circuit_drone + +/obj/item/shell/server + name = "server assembly" + icon_state = "setup_stationary-open" + shell_to_spawn = /obj/structure/server + screw_delay = 10 SECONDS + +/obj/item/shell/airlock + name = "circuit airlock assembly" + icon = 'icons/obj/doors/airlocks/station/public.dmi' + icon_state = "construction" + shell_to_spawn = /obj/machinery/door/airlock/shell + screw_delay = 10 SECONDS + +/obj/item/shell/bci + name = "brain-computer interface assembly" + icon_state = "bci-open" + shell_to_spawn = /obj/item/organ/cyberimp/bci + +/obj/item/shell/scanner_gate + name = "scanner gate assembly" + icon = 'icons/obj/machines/scangate.dmi' + icon_state = "scangate_black_open" + shell_to_spawn = /obj/structure/scanner_gate_shell diff --git a/config/entries/logging.txt b/config/entries/logging.txt index 1690dbfacf..31692afccb 100644 --- a/config/entries/logging.txt +++ b/config/entries/logging.txt @@ -13,6 +13,9 @@ LOG_SAY ## log client access (logon/logoff) LOG_ACCESS +## Enables log entries for logins that failed due to suspicious circumstances (banned player, CID randomiser, spoofed BYOND version, etc.) to a dedicated file. +LOG_SUSPICIOUS_LOGIN + ## log game actions (start of round, results, etc.) LOG_GAME diff --git a/tgstation.dme b/tgstation.dme index 7a8a7a173e..b9ca238787 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -3610,6 +3610,7 @@ #include "code\modules\tgs\includes.dm" #include "code\modules\tgui\external.dm" #include "code\modules\tgui\states.dm" +#include "code\modules\tgui\status_composers.dm" #include "code\modules\tgui\tgui.dm" #include "code\modules\tgui\tgui_alert.dm" #include "code\modules\tgui\tgui_input_list.dm"