From 7af7d3187b1dc5bfa29d2197fadda7a21b25f529 Mon Sep 17 00:00:00 2001 From: Geeves Date: Sun, 22 Nov 2020 11:57:16 +0200 Subject: [PATCH] Remote Canisters (#10563) --- code/__defines/obj.dm | 1 + code/_helpers/logging.dm | 8 ++++ code/controllers/configuration.dm | 4 ++ code/game/machinery/atmoalter/canister.dm | 35 +++++++++++--- .../atmoalter/portable_atmospherics.dm | 9 +++- code/game/machinery/machinery.dm | 43 ++++++++++++++++++ code/global.dm | 1 + code/modules/admin/admin_verbs.dm | 1 + code/modules/admin/verbs/getlogs.dm | 13 ++++++ code/modules/assembly/signaler.dm | 17 +++++-- code/modules/power/singularity/emitter.dm | 19 +------- config/example/config.txt | 3 ++ html/changelogs/geeves-remote_canisters.yml | 7 +++ icons/obj/atmos.dmi | Bin 24091 -> 24197 bytes 14 files changed, 131 insertions(+), 30 deletions(-) create mode 100644 html/changelogs/geeves-remote_canisters.yml diff --git a/code/__defines/obj.dm b/code/__defines/obj.dm index a2b6ac18afa..698102a16f7 100644 --- a/code/__defines/obj.dm +++ b/code/__defines/obj.dm @@ -1,5 +1,6 @@ #define OBJ_FLAG_ROTATABLE (1<<1) //Can this object be rotated? #define OBJ_FLAG_ROTATABLE_ANCHORED (1<<2) // This object can be rotated even while anchored +#define OBJ_FLAG_SIGNALER (1<<3) // Can this take a signaler? only in use for machinery /obj/proc/iswrench() return FALSE diff --git a/code/_helpers/logging.dm b/code/_helpers/logging.dm index 55a08f5e634..6bae7006b25 100644 --- a/code/_helpers/logging.dm +++ b/code/_helpers/logging.dm @@ -50,6 +50,14 @@ game_log("ADMIN", text) send_gelf_log(short_message=text, long_message="[time_stamp()]: [text]",level=level,category="ADMIN",additional_data=list("_ckey"=html_encode(ckey),"_admin_key"=html_encode(admin_key),"_ckey_target"=html_encode(ckey_target))) +/proc/log_signal(var/text) + if(length(signal_log) >= 100) + signal_log.Cut(1, 2) + signal_log.Add("|[time_stamp()]| [text]") + if(config.log_signaler) + game_log("SIGNALER", text) + send_gelf_log(short_message=text, long_message="[time_stamp()]: [text]",level=SEVERITY_NOTICE,category="SIGNALER") + /proc/log_debug(text,level = SEVERITY_DEBUG) if (config.log_debug) game_log("DEBUG", text) diff --git a/code/controllers/configuration.dm b/code/controllers/configuration.dm index 91714717f02..38029204416 100644 --- a/code/controllers/configuration.dm +++ b/code/controllers/configuration.dm @@ -8,6 +8,7 @@ var/list/gamemode_cache = list() var/log_access = 0 // log login/logout var/log_say = 0 // log client say var/log_admin = 0 // log admin actions + var/log_signaler = 0 // log signaler actions var/log_debug = 1 // log debug output var/log_game = 0 // log game events var/log_vote = 0 // log voting @@ -405,6 +406,9 @@ var/list/gamemode_cache = list() if ("log_admin") config.log_admin = 1 + if ("log_signaler") + config.log_signaler = 1 + if ("log_debug") config.log_debug = text2num(value) diff --git a/code/game/machinery/atmoalter/canister.dm b/code/game/machinery/atmoalter/canister.dm index a00b498ce03..0f53287bbac 100644 --- a/code/game/machinery/atmoalter/canister.dm +++ b/code/game/machinery/atmoalter/canister.dm @@ -11,6 +11,7 @@ density = 1 var/health = 100.0 flags = CONDUCT + obj_flags = OBJ_FLAG_SIGNALER w_class = ITEMSIZE_HUGE var/valve_open = 0 @@ -131,6 +132,9 @@ else update_flag |= 32 + if(signaler) + update_flag |= 64 + if(update_flag == old_flag) return 1 else @@ -162,6 +166,9 @@ update_flag cut_overlays() set_light(FALSE) + if(signaler) + add_overlay("signaler") + if(update_flag & 1) add_overlay("can-open") if(update_flag & 2) @@ -182,7 +189,6 @@ update_flag var/mutable_appearance/indicator_overlay = mutable_appearance(icon, "can-o3", EFFECTS_ABOVE_LIGHTING_LAYER) add_overlay(indicator_overlay) set_light(1.4, 1, COLOR_BRIGHT_GREEN) - return /obj/machinery/portable_atmospherics/canister/fire_act(datum/gas_mixture/air, exposed_temperature, exposed_volume) if(exposed_temperature > temperature_resistance) @@ -191,21 +197,25 @@ update_flag /obj/machinery/portable_atmospherics/canister/proc/healthcheck() if(destroyed) - return 1 + return TRUE if (src.health <= 10) var/atom/location = src.loc location.assume_air(air_contents) - src.destroyed = 1 + destroyed = TRUE + obj_flags &= ~OBJ_FLAG_SIGNALER playsound(src.loc, 'sound/effects/spray.ogg', 10, 1, -3) - src.density = 0 - update_icon() + density = FALSE if (src.holding) src.holding.forceMove(src.loc) src.holding = null + detach_signaler() + + update_icon() + return 1 else return 1 @@ -285,12 +295,17 @@ update_flag valve_open = !valve_open /obj/machinery/portable_atmospherics/canister/attackby(var/obj/item/W as obj, var/mob/user as mob) - if(!W.iswrench() && !istype(W, /obj/item/tank) && !istype(W, /obj/item/device/analyzer) && !istype(W, /obj/item/modular_computer)) - visible_message("\The [user] hits \the [src] with \a [W]!") + if(!W.iswrench() && !istype(W, /obj/item/tank) && !istype(W, /obj/item/device/analyzer) && !istype(W, /obj/item/modular_computer) && !issignaler(W) && !(W.iswirecutter() && signaler)) + if(W.flags & NOBLUDGEON) + return + visible_message(SPAN_WARNING("\The [user] hits \the [src] with \the [W]!"), SPAN_NOTICE("You hit \the [src] with \the [W].")) + user.do_attack_animation(src, W) + playsound(src, 'sound/weapons/smash.ogg', 60, 1) src.health -= W.force if(!istype(W, /obj/item/forensics)) src.add_fingerprint(user) healthcheck() + return if(istype(user, /mob/living/silicon/robot) && istype(W, /obj/item/tank/jetpack)) var/datum/gas_mixture/thejetpack = W:air_contents @@ -307,6 +322,7 @@ update_flag ..() + update_icon() SSnanoui.update_uis(src) // Update all NanoUIs attached to src /obj/machinery/portable_atmospherics/canister/attack_ai(var/mob/user as mob) @@ -415,6 +431,11 @@ update_flag return 1 +/obj/machinery/portable_atmospherics/canister/do_signaler() + valve_open = !valve_open + if(valve_open) + log_open_userless("a signaler") + /obj/machinery/portable_atmospherics/canister/phoron/Initialize() . = ..() diff --git a/code/game/machinery/atmoalter/portable_atmospherics.dm b/code/game/machinery/atmoalter/portable_atmospherics.dm index 286f07dfade..58357409a9c 100644 --- a/code/game/machinery/atmoalter/portable_atmospherics.dm +++ b/code/game/machinery/atmoalter/portable_atmospherics.dm @@ -133,8 +133,7 @@ A.analyze_gases(src, user) return - return - + return ..() /obj/machinery/portable_atmospherics/powered @@ -195,3 +194,9 @@ log_admin("[user] ([user.ckey]) opened '[src.name]' containing [gases].", ckey=key_name(user)) message_admins("[key_name_admin(user)] opened '[src.name]' containing [gases]. (JMP)") + +/obj/machinery/portable_atmospherics/proc/log_open_userless(var/cause) + if(air_contents.gas.len == 0) + return + + message_admins("'[src.name]' was opened[cause ? " by [cause]" : ""], containing [english_list(air_contents.gas)]. (JMP)") \ No newline at end of file diff --git a/code/game/machinery/machinery.dm b/code/game/machinery/machinery.dm index 19161455e55..d454dcbe550 100644 --- a/code/game/machinery/machinery.dm +++ b/code/game/machinery/machinery.dm @@ -164,6 +164,11 @@ Class Procs: return ..() +/obj/machinery/examine(mob/user) + . = ..() + if(signaler && Adjacent(user)) + to_chat(user, SPAN_WARNING("\The [src] has a hidden signaler attached to it.")) + /obj/machinery/proc/machinery_process() //If you dont use process or power why are you here if(!(use_power || idle_power_usage || active_power_usage)) return PROCESS_KILL @@ -280,6 +285,44 @@ Class Procs: return ..() +/obj/machinery/attackby(obj/item/W, mob/user) + if(obj_flags & OBJ_FLAG_SIGNALER) + if(issignaler(W)) + if(signaler) + to_chat(user, SPAN_WARNING("\The [src] already has a signaler attached.")) + return + var/obj/item/device/assembly/signaler/S = W + user.drop_from_inventory(W, src) + signaler = S + S.machine = src + user.visible_message("[user] attaches \the [S] to \the [src].", SPAN_NOTICE("You attach \the [S] to \the [src]."), range = 3) + log_and_message_admins("has attached a signaler to \the [src].", user, get_turf(src)) + return + else if(W.iswirecutter() && signaler) + user.visible_message("[user] removes \the [signaler] from \the [src].", SPAN_NOTICE("You remove \the [signaler] from \the [src]."), range = 3) + user.put_in_hands(detach_signaler()) + return + + return ..() + +/obj/machinery/proc/detach_signaler(var/turf/detach_turf) + if(!signaler) + return + + if(!detach_turf) + detach_turf = get_turf(src) + if(!detach_turf) + log_debug("[src] tried to drop a signaler, but it had no turf ([src.x]-[src.y]-[src.z])") + return + + var/obj/item/device/assembly/signaler/S = signaler + + signaler.forceMove(detach_turf) + signaler.machine = null + signaler = null + + return S + /obj/machinery/proc/RefreshParts() /obj/machinery/proc/assign_uid() diff --git a/code/global.dm b/code/global.dm index ec92e970fed..a442f7ebfb3 100644 --- a/code/global.dm +++ b/code/global.dm @@ -40,6 +40,7 @@ var/host = null //only here until check @ code\modules\ghosttrap\trap.dm:112 is var/list/jobMax = list() var/list/bombers = list() var/list/admin_log = list() +var/list/signal_log = list() var/list/lastsignalers = list() // Keeps last 100 signals here in format: "[src] used \ref[src] @ location [src.loc]: [freq]/[code]" var/list/lawchanges = list() // Stores who uploaded laws to which silicon-based lifeform, and what the law was. var/list/reg_dna = list() diff --git a/code/modules/admin/admin_verbs.dm b/code/modules/admin/admin_verbs.dm index e6f82a4c239..c381f73e6cb 100644 --- a/code/modules/admin/admin_verbs.dm +++ b/code/modules/admin/admin_verbs.dm @@ -30,6 +30,7 @@ var/list/admin_verbs_admin = list( /datum/admins/proc/access_news_network, /*allows access of newscasters*/ /client/proc/giveruntimelog, /*allows us to give access to runtime logs to somebody*/ /client/proc/getserverlog, /*allows us to fetch server logs (diary) for other days*/ + /client/proc/view_signal_log, /*allows admins to check the log of signaler uses*/ /client/proc/jumptocoord, /*we ghost and jump to a coordinate*/ /client/proc/Getmob, /*teleports a mob to our location*/ /client/proc/Getkey, /*teleports a mob with a certain ckey to our location*/ diff --git a/code/modules/admin/verbs/getlogs.dm b/code/modules/admin/verbs/getlogs.dm index 731fa95b71b..0e6af92039a 100644 --- a/code/modules/admin/verbs/getlogs.dm +++ b/code/modules/admin/verbs/getlogs.dm @@ -78,3 +78,16 @@ //Other log stuff put here for the sake of organisation + +/client/proc/view_signal_log() + set name = "View Signaler Log" + set desc = "Use this to view who sent signaler signals to things." + set category = "Admin" + + var/text_signal_log = "" + for(var/log in signal_log) + text_signal_log += "[log]
" + + var/datum/browser/signal_win = new(usr, "signallog", "Signal Log", 550, 500) + signal_win.set_content(text_signal_log) + signal_win.open() \ No newline at end of file diff --git a/code/modules/assembly/signaler.dm b/code/modules/assembly/signaler.dm index 7e90e173a30..1f56ed070e8 100644 --- a/code/modules/assembly/signaler.dm +++ b/code/modules/assembly/signaler.dm @@ -67,12 +67,12 @@ if(href_list["send"]) spawn( 0 ) - signal() + signal(usr) var/datum/vueui/ui = SSvueui.get_open_ui(usr, src) ui.check_for_change() -/obj/item/device/assembly/signaler/proc/signal() +/obj/item/device/assembly/signaler/proc/signal(var/mob/user) if(!radio_connection) return if(within_jamming_range(src)) @@ -81,6 +81,8 @@ signal.source = src signal.encryption = code signal.data["message"] = "ACTIVATE" + if(user) + signal.data["user"] = WEAKREF(user) radio_connection.post_signal(src, signal) return @@ -111,6 +113,15 @@ pulse(TRUE) if(machine) + var/found_user = FALSE + if(signal.data["user"]) + var/datum/weakref/user_ref = signal.data["user"] + var/mob/user = user_ref.resolve() + if(user) + found_user = TRUE + log_signal("[key_name(user)] has sent a signal to [machine.name] ([machine.x]-[machine.y]-[machine.z])") + if(!found_user) + log_signal("A userless signaler has sent a signal to [machine.name] ([machine.x]-[machine.y]-[machine.z])") machine.audible_message("[icon2html(machine, viewers(get_turf(machine)))] [capitalize_first_letters(src.name)] beeps, \"Beep beep!\"") else if(!holder) audible_message("[icon2html(src, viewers(get_turf(src)))] [capitalize_first_letters(src.name)] beeps, \"Beep beep!\"") @@ -154,7 +165,7 @@ if(use_check_and_message(user)) return to_chat(user, SPAN_NOTICE("You click \the [src]'s signal button.")) - signal() + signal(user) /obj/item/device/assembly/signaler/proc/deadman_trigger(var/mob/user) if(deadman) //If its not activated, there is no point in triggering it diff --git a/code/modules/power/singularity/emitter.dm b/code/modules/power/singularity/emitter.dm index d53ba8fc0cf..05307743f43 100644 --- a/code/modules/power/singularity/emitter.dm +++ b/code/modules/power/singularity/emitter.dm @@ -11,7 +11,7 @@ anchored = FALSE density = TRUE req_access = list(access_engine_equip) - obj_flags = OBJ_FLAG_ROTATABLE + obj_flags = OBJ_FLAG_ROTATABLE | OBJ_FLAG_SIGNALER var/id use_power = 0 //uses powernet power, not APC power @@ -47,8 +47,6 @@ to_chat(user, SPAN_WARNING("\The [src] is bolted and welded to the floor, and ready to fire.")) if(Adjacent(user)) to_chat(user, SPAN_NOTICE("The shot counter display reads: [shot_counter]")) - if(signaler) - to_chat(user, SPAN_WARNING("\The [src] has a hidden signaler attached to it.")) /obj/machinery/power/emitter/Destroy() if(special_emitter) @@ -163,21 +161,6 @@ shot_counter++ /obj/machinery/power/emitter/attackby(obj/item/W, mob/user) - if(istype(W, /obj/item/device/assembly/signaler)) - var/obj/item/device/assembly/signaler/S = W - user.drop_from_inventory(W, src) - signaler = S - S.machine = src - user.visible_message(SPAN_WARNING("\The [user] attaches \the [S] to \the [src]."), - SPAN_NOTICE("You attach \the [S] to \the [src].")) - return - if(W.iswirecutter() && signaler) - signaler.forceMove(get_turf(user)) - signaler.machine = null - user.visible_message(SPAN_WARNING("\The [user] removes \the [signaler] from \the [src]."), - SPAN_NOTICE("You remove \the [signaler] to \the [src].")) - signaler = null - return if(W.iswrench()) if(active) to_chat(user, SPAN_WARNING("You cannot unbolt \the [src] while it's active.")) diff --git a/config/example/config.txt b/config/example/config.txt index 2d0bf2de89e..47d71d6088c 100644 --- a/config/example/config.txt +++ b/config/example/config.txt @@ -54,6 +54,9 @@ LOG_SAY ## log admin actions LOG_ADMIN +## log signaler actions +LOG_SIGNALER + ## log client access (logon/logoff) LOG_ACCESS diff --git a/html/changelogs/geeves-remote_canisters.yml b/html/changelogs/geeves-remote_canisters.yml new file mode 100644 index 00000000000..390e01bd9db --- /dev/null +++ b/html/changelogs/geeves-remote_canisters.yml @@ -0,0 +1,7 @@ +author: Geeves + +delete-after: True + +changes: + - rscadd: "You can now attach signalers to canisters. When activated, the signaler will toggle the valve open or shut." + - tweak: "Attacking a canister now has a sound and an animation." \ No newline at end of file diff --git a/icons/obj/atmos.dmi b/icons/obj/atmos.dmi index 5e8d5a28a07e40299cfe5b4b733f48611f7432d7..7ddebb5e7478cb339875ff8c60d8bac933ff77e3 100644 GIT binary patch delta 4094 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