diff --git a/code/__DEFINES/dcs/signals/signals_datum.dm b/code/__DEFINES/dcs/signals/signals_datum.dm index a8da7781c37..be48a21bfb3 100644 --- a/code/__DEFINES/dcs/signals/signals_datum.dm +++ b/code/__DEFINES/dcs/signals/signals_datum.dm @@ -42,9 +42,3 @@ #define COMSIG_GASMIX_REMOVED "comsig_gasmix_removed" /// From /datum/gas_mixture/proc/react: () #define COMSIG_GASMIX_REACTED "comsig_gasmix_reacted" - -// Modular computer's file signals. Tells the program datum something is going on. -/// From /obj/item/modular_computer/proc/store_file: (datum/computer_file/file_source, obj/item/modular_computer/host) -#define COMSIG_MODULAR_COMPUTER_FILE_STORE "comsig_modular_computer_file_store" -/// From /obj/item/modular_computer/proc/store_file: () -#define COMSIG_MODULAR_COMPUTER_FILE_DELETED "comsig_modular_computer_file_deleted" diff --git a/code/__DEFINES/dcs/signals/signals_modular_computer.dm b/code/__DEFINES/dcs/signals/signals_modular_computer.dm new file mode 100644 index 00000000000..950262a5fe1 --- /dev/null +++ b/code/__DEFINES/dcs/signals/signals_modular_computer.dm @@ -0,0 +1,15 @@ +// Various modular computer signals. + +/// From /obj/item/modular_computer/proc/store_file: (datum/computer_file/file_storing) +#define COMSIG_MODULAR_COMPUTER_FILE_STORE "comsig_modular_computer_file_store" +/// From /obj/item/modular_computer/proc/remove_file: (datum/computer_file/file_removing) +#define COMSIG_MODULAR_COMPUTER_FILE_DELETE "comsig_modular_computer_file_delete" +/// From /obj/item/modular_computer/proc/store_file: (datum/computer_file/file_source, obj/item/modular_computer/host) +#define COMSIG_COMPUTER_FILE_STORE "comsig_computer_file_store" +/// From /obj/item/modular_computer/proc/store_file: () +#define COMSIG_COMPUTER_FILE_DELETE "comsig_computer_file_delete" + +/// from /obj/item/modular_computer/imprint_id(): (name, job) +#define COMSIG_MODULAR_PDA_IMPRINT_UPDATED "comsig_modular_pda_imprint_updated" +/// from /obj/item/modular_computer/reset_id(): () +#define COMSIG_MODULAR_PDA_IMPRINT_RESET "comsig_modular_pda_imprint_reset" diff --git a/code/__HELPERS/chat_filter.dm b/code/__HELPERS/chat_filter.dm index eff2be71c96..b395da3d7aa 100644 --- a/code/__HELPERS/chat_filter.dm +++ b/code/__HELPERS/chat_filter.dm @@ -14,14 +14,14 @@ return null -/// Given a text, will return what word is on the IC filter, ignoring words allowed on the PDA, with the reason. +/// Given a text, will return what word is on the soft IC filter, with the reason. /// Returns null if the message is OK. -/proc/is_ic_filtered_for_pdas(message) - if (config.ic_outside_pda_filter_regex?.Find(message)) - var/matched_group = GET_MATCHED_GROUP(config.ic_outside_pda_filter_regex) +/proc/is_soft_ic_filtered(message) + if (config.soft_ic_filter_regex?.Find(message)) + var/matched_group = GET_MATCHED_GROUP(config.soft_ic_filter_regex) return list( matched_group, - config.ic_filter_reasons[matched_group] || config.shared_filter_reasons[matched_group], + config.soft_ic_filter_reasons[matched_group] || config.soft_ic_outside_pda_filter_reasons[matched_group] || config.soft_shared_filter_reasons[matched_group], ) return null @@ -35,14 +35,40 @@ return null -/// Given a text, will return what word is on the soft IC filter, with the reason. +/// Given a text, will return that word is on the soft OOC filter, with the reason. /// Returns null if the message is OK. -/proc/is_soft_ic_filtered(message) - if (config.soft_ic_filter_regex?.Find(message)) - var/matched_group = GET_MATCHED_GROUP(config.soft_ic_filter_regex) +/proc/is_soft_ooc_filtered(message) + if (config.soft_ooc_filter_regex?.Find(message)) + var/matched_group = GET_MATCHED_GROUP(config.soft_ooc_filter_regex) + return list(matched_group, config.soft_shared_filter_reasons[matched_group]) + + return null + +/// Checks a PDA message against the IC/Soft IC filter. Returns TRUE if the message should be sent. +/// Notifies the user passed in arguments if the message matched either filter. +/proc/check_pda_message_against_filter(message, mob/user) + var/list/filter_result = CAN_BYPASS_FILTER(user) ? null : is_ic_filtered_for_pdas(message) + if (filter_result) + REPORT_CHAT_FILTER_TO_USER(user, filter_result) + return FALSE + + var/list/soft_filter_result = CAN_BYPASS_FILTER(user) ? null : is_soft_ic_filtered_for_pdas(message) + if (soft_filter_result) + if(tgui_alert(user, "Your message contains \"[soft_filter_result[CHAT_FILTER_INDEX_WORD]]\". \"[soft_filter_result[CHAT_FILTER_INDEX_REASON]]\", Are you sure you want to send it?", "Soft Blocked Word", list("Yes", "No")) != "Yes") + return FALSE + message_admins("[ADMIN_LOOKUPFLW(user)] has passed the soft filter for \"[soft_filter_result[CHAT_FILTER_INDEX_WORD]]\" they may be using a disallowed term in PDA messages. Message: \"[message]\"") + log_admin_private("[key_name(user)] has passed the soft filter for \"[soft_filter_result[CHAT_FILTER_INDEX_WORD]]\" they may be using a disallowed term in PDA messages. Message: \"[message]\"") + + return TRUE + +/// Given a text, will return what word is on the IC filter, ignoring words allowed on the PDA, with the reason. +/// Returns null if the message is OK. +/proc/is_ic_filtered_for_pdas(message) + if (config.ic_outside_pda_filter_regex?.Find(message)) + var/matched_group = GET_MATCHED_GROUP(config.ic_outside_pda_filter_regex) return list( matched_group, - config.soft_ic_filter_reasons[matched_group] || config.soft_ic_outside_pda_filter_reasons[matched_group] || config.soft_shared_filter_reasons[matched_group], + config.ic_filter_reasons[matched_group] || config.shared_filter_reasons[matched_group], ) return null @@ -59,15 +85,6 @@ return null -/// Given a text, will return that word is on the soft OOC filter, with the reason. -/// Returns null if the message is OK. -/proc/is_soft_ooc_filtered(message) - if (config.soft_ooc_filter_regex?.Find(message)) - var/matched_group = GET_MATCHED_GROUP(config.soft_ooc_filter_regex) - return list(matched_group, config.soft_shared_filter_reasons[matched_group]) - - return null - /// Logs to the filter log with the given message, match, and scope /proc/log_filter(scope, message, filter_result) log_filter_raw("[scope] filter:\n\tMessage: [message]\n\tFilter match: [filter_result[CHAT_FILTER_INDEX_WORD]]") diff --git a/code/__HELPERS/cmp.dm b/code/__HELPERS/cmp.dm index b21cbe22ccb..e747da43c4b 100644 --- a/code/__HELPERS/cmp.dm +++ b/code/__HELPERS/cmp.dm @@ -141,11 +141,11 @@ /proc/cmp_typepaths_asc(A, B) return sorttext("[B]","[A]") -/proc/cmp_pdaname_asc(obj/item/modular_computer/A, obj/item/modular_computer/B) - return sorttext(B?.saved_identification, A?.saved_identification) +/proc/cmp_pdaname_asc(datum/computer_file/program/messenger/A, datum/computer_file/program/messenger/B) + return sorttext(B?.computer?.saved_identification, A?.computer?.saved_identification) -/proc/cmp_pdajob_asc(obj/item/modular_computer/A, obj/item/modular_computer/B) - return sorttext(B?.saved_job, A?.saved_job) +/proc/cmp_pdajob_asc(datum/computer_file/program/messenger/A, datum/computer_file/program/messenger/B) + return sorttext(B?.computer?.saved_job, A?.computer?.saved_job) /proc/cmp_num_string_asc(A, B) return text2num(A) - text2num(B) diff --git a/code/controllers/subsystem/modular_computers.dm b/code/controllers/subsystem/modular_computers.dm index 0c4d88c55e3..ad0be83bb52 100644 --- a/code/controllers/subsystem/modular_computers.dm +++ b/code/controllers/subsystem/modular_computers.dm @@ -19,6 +19,7 @@ SUBSYSTEM_DEF(modular_computers) var/intrusion_detection_enabled = TRUE ///Boolean to show a message warning if there's an active intrusion for Wirecarp users. var/intrusion_detection_alarm = FALSE + var/next_picture_id = 0 /datum/controller/subsystem/modular_computers/Initialize() build_software_lists() @@ -77,4 +78,13 @@ SUBSYSTEM_DEF(modular_computers) logs = list() add_log("-!- LOGS DELETED BY SYSTEM OPERATOR -!-") +/** + * Returns a name which a /datum/picture can be assigned to. + * Use this function to get asset names and to avoid cache duplicates/overwriting. + */ +/datum/controller/subsystem/modular_computers/proc/get_next_picture_name() + var/next_uid = next_picture_id + next_picture_id++ + return "ntos_picture_[next_uid].png" + #undef MAX_LOG_COUNT diff --git a/code/datums/records/crime.dm b/code/datums/records/crime.dm index 77e3d5b1001..a22ce7c816a 100644 --- a/code/datums/records/crime.dm +++ b/code/datums/records/crime.dm @@ -47,16 +47,17 @@ /// Sends a citation alert message to the target's PDA. /datum/crime/citation/proc/alert_owner(mob/sender, atom/source, target_name, message) - for(var/obj/item/modular_computer/tablet in GLOB.TabletMessengers) - if(tablet.saved_identification != target_name) + for(var/messenger_ref in GLOB.pda_messengers) + var/datum/computer_file/program/messenger/messenger = GLOB.pda_messengers[messenger_ref] + if(messenger.computer.saved_identification != target_name) continue - var/datum/signal/subspace/messaging/tablet_msg/signal = new(source, list( - name = "Security Citation", - job = "Citation Server", - message = message, - targets = list(tablet), - automated = TRUE + var/datum/signal/subspace/messaging/tablet_message/signal = new(source, list( + "fakename" = "Security Citation", + "fakejob" = "Citation Server", + "message" = message, + "targets" = list(messenger), + "automated" = TRUE )) signal.send_to_receivers() sender.log_message("(PDA: Citation Server) sent \"[message]\" to [signal.format_target()]", LOG_PDA) diff --git a/code/game/machinery/PDApainter.dm b/code/game/machinery/PDApainter.dm index ad6104fda75..9c55051ca60 100644 --- a/code/game/machinery/PDApainter.dm +++ b/code/game/machinery/PDApainter.dm @@ -335,6 +335,12 @@ stored_pda.icon_state = initial(pda_path.icon_state) stored_pda.desc = initial(pda_path.desc) + return TRUE + if("reset_pda") + if((machine_stat & BROKEN) || !stored_pda) + return TRUE + + stored_pda.reset_imprint() return TRUE if("trim_card") if((machine_stat & BROKEN) || !stored_id_card) diff --git a/code/game/machinery/telecomms/computers/message.dm b/code/game/machinery/telecomms/computers/message.dm index 4db37cea2d8..e2a669804a1 100644 --- a/code/game/machinery/telecomms/computers/message.dm +++ b/code/game/machinery/telecomms/computers/message.dm @@ -210,14 +210,16 @@ var/job = tgui_input_text(usr, "What is the sender's job?", "Job") var/recipient + var/list/tablet_to_messenger = list() var/list/viewable_tablets = list() - for (var/obj/item/modular_computer/tablet as anything in GLOB.TabletMessengers) - var/datum/computer_file/program/messenger/message_app = locate() in tablet.stored_files + for (var/messenger_ref in GLOB.pda_messengers) + var/datum/computer_file/program/messenger/message_app = GLOB.pda_messengers[messenger_ref] if(!message_app || message_app.invisible) continue - if(!tablet.saved_identification) + if(!message_app.computer.saved_identification) continue - viewable_tablets += tablet + viewable_tablets += message_app.computer + tablet_to_messenger[message_app.computer] = message_app if(length(viewable_tablets) > 0) recipient = tgui_input_list(usr, "Select a tablet from the list", "Tablet Selection", viewable_tablets) else @@ -234,14 +236,11 @@ notice_message = "NOTICE: No message entered!" return attack_hand(usr) - var/datum/signal/subspace/messaging/tablet_msg/signal = new(src, list( - "name" = "[sender]", - "job" = "[job]", - "message" = html_decode(message), - "ref" = REF(src), - "targets" = list(recipient), - "rigged" = FALSE, - "automated" = FALSE, + var/datum/signal/subspace/messaging/tablet_message/signal = new(src, list( + "fakename" = "[sender]", + "fakejob" = "[job]", + "message" = message, + "targets" = list(tablet_to_messenger[recipient]), )) // This will log the signal and transmit it to the target linkedServer.receive_information(signal, null) diff --git a/code/game/machinery/telecomms/machines/broadcaster.dm b/code/game/machinery/telecomms/machines/broadcaster.dm index 8f0ccea7a86..56b53437e63 100644 --- a/code/game/machinery/telecomms/machines/broadcaster.dm +++ b/code/game/machinery/telecomms/machines/broadcaster.dm @@ -25,6 +25,9 @@ GLOBAL_VAR_INIT(message_delay, 0) // To make sure restarting the recentmessages return if(!signal.data["message"]) return + var/signal_message = "[signal.frequency]:[signal.data["message"]]:[signal.data["name"]]" + if(signal_message in GLOB.recentmessages) + return // Prevents massive radio spam signal.mark_done() @@ -36,9 +39,6 @@ GLOBAL_VAR_INIT(message_delay, 0) // To make sure restarting the recentmessages if (T) signal.levels |= SSmapping.get_connected_levels(T) - var/signal_message = "[signal.frequency]:[signal.data["message"]]:[signal.data["name"]]" - if(signal_message in GLOB.recentmessages) - return GLOB.recentmessages.Add(signal_message) if(signal.data["slow"] > 0) diff --git a/code/game/machinery/telecomms/machines/message_server.dm b/code/game/machinery/telecomms/machines/message_server.dm index b721e7681f5..1e012d10027 100644 --- a/code/game/machinery/telecomms/machines/message_server.dm +++ b/code/game/machinery/telecomms/machines/message_server.dm @@ -131,14 +131,12 @@ return // log the signal - if(istype(signal, /datum/signal/subspace/messaging/tablet_msg)) - var/datum/signal/subspace/messaging/tablet_msg/PDAsignal = signal - var/datum/data_tablet_msg/msg = new(PDAsignal.format_target(), "[PDAsignal.data["name"]] ([PDAsignal.data["job"]])", PDAsignal.data["message"], PDAsignal.data["photo"]) - pda_msgs += msg - signal.logged = msg + if(istype(signal, /datum/signal/subspace/messaging/tablet_message)) + var/datum/signal/subspace/messaging/tablet_message/PDAsignal = signal + var/datum/data_tablet_msg/log_message = new(PDAsignal.format_target(), PDAsignal.format_sender(), PDAsignal.format_message(), PDAsignal.format_photo_path()) + pda_msgs += log_message else if(istype(signal, /datum/signal/subspace/messaging/rc)) var/datum/data_rc_msg/msg = new(signal.data["rec_dpt"], signal.data["send_dpt"], signal.data["message"], signal.data["stamped"], signal.data["verified"], signal.data["priority"]) - signal.logged = msg if(signal.data["send_dpt"]) // don't log messages not from a department but allow them to work rc_msgs += msg signal.data["reject"] = FALSE @@ -158,7 +156,6 @@ /datum/signal/subspace/messaging frequency = FREQ_COMMON server_type = /obj/machinery/telecomms/message_server - var/datum/logged /datum/signal/subspace/messaging/New(init_source, init_data) source = init_source @@ -175,28 +172,31 @@ return copy // Tablet message signal datum -/datum/signal/subspace/messaging/tablet_msg/proc/format_target() - if (length(data["targets"]) > 1) +/datum/signal/subspace/messaging/tablet_message/proc/format_target() + if (data["everyone"]) return "Everyone" - var/obj/item/modular_computer/target = data["targets"][1] + var/datum/computer_file/program/messenger/target_app = data["targets"][1] + var/obj/item/modular_computer/target = target_app.computer return "[target.saved_identification] ([target.saved_job])" -/datum/signal/subspace/messaging/tablet_msg/proc/format_message() - return "\"[data["message"]]\"" +/datum/signal/subspace/messaging/tablet_message/proc/format_sender() + var/display_name = get_messenger_name(locate(data["ref"])) + return display_name ? display_name : STRINGIFY_PDA_TARGET(data["fakename"], data["fakejob"]) -/datum/signal/subspace/messaging/tablet_msg/broadcast() - if (!logged) // Can only go through if a message server logs it - return - for (var/obj/item/modular_computer/comp in data["targets"]) - if(!QDELETED(comp)) - for(var/datum/computer_file/program/messenger/app in comp.stored_files) - if(!QDELETED(app)) - app.receive_message(src) +/datum/signal/subspace/messaging/tablet_message/proc/format_message() + return data["message"] + +/datum/signal/subspace/messaging/tablet_message/proc/format_photo_path() + return data["photo"] + +/datum/signal/subspace/messaging/tablet_message/broadcast() + for (var/datum/computer_file/program/messenger/app in data["targets"]) + if(!QDELETED(app)) + app.receive_message(src) + data["targets"] = null // Request Console signal datum /datum/signal/subspace/messaging/rc/broadcast() - if (!logged) // Like /pda, only if logged - return var/recipient_department = ckey(data["recipient_department"]) for (var/obj/machinery/requests_console/console in GLOB.req_console_all) if(ckey(console.department) == recipient_department || (data["ore_update"] && console.receive_ore_updates)) @@ -207,8 +207,8 @@ var/sender = "Unspecified" var/recipient = "Unspecified" var/message = "Blank" // transferred message - var/datum/picture/picture // attached photo - var/automated = 0 //automated message + var/picture_asset_key // attached photo path + var/automated = FALSE // automated message /datum/data_tablet_msg/New(param_rec, param_sender, param_message, param_photo) if(param_rec) @@ -218,18 +218,7 @@ if(param_message) message = param_message if(param_photo) - picture = param_photo - -/datum/data_tablet_msg/Topic(href,href_list) - ..() - if(href_list["photo"]) - var/mob/M = usr - M << browse_rsc(picture.picture_image, "pda_photo.png") - M << browse("
" \
- + "", "window=pdaphoto;size=[picture.psize_x]x[picture.psize_y];can-close=true")
- onclose(M, "pdaphoto")
+ picture_asset_key = param_photo
/datum/data_rc_msg
var/rec_dpt = "Unspecified" // receiving department
diff --git a/code/game/objects/items/devices/radio/radio.dm b/code/game/objects/items/devices/radio/radio.dm
index 221a418c1c3..3c7880ea011 100644
--- a/code/game/objects/items/devices/radio/radio.dm
+++ b/code/game/objects/items/devices/radio/radio.dm
@@ -303,11 +303,8 @@
channel = null
// Nearby active jammers prevent the message from transmitting
- var/turf/position = get_turf(src)
- for(var/obj/item/jammer/jammer as anything in GLOB.active_jammers)
- var/turf/jammer_turf = get_turf(jammer)
- if(position?.z == jammer_turf.z && (get_dist(position, jammer_turf) <= jammer.range) && !syndie)
- return
+ if(is_within_radio_jammer_range(src) && !syndie)
+ return
// Determine the identity information which will be attached to the signal.
var/atom/movable/virtualspeaker/speaker = new(null, talking_movable, src)
diff --git a/code/game/objects/items/devices/traitordevices.dm b/code/game/objects/items/devices/traitordevices.dm
index d0176835931..0e905f4a8ee 100644
--- a/code/game/objects/items/devices/traitordevices.dm
+++ b/code/game/objects/items/devices/traitordevices.dm
@@ -274,6 +274,12 @@ effective or pretty fucking useless.
animate(user,alpha = clamp(255 - charge,0,255),time = 10)
+/// Checks if a given atom is in range of a radio jammer, returns TRUE if it is.
+/proc/is_within_radio_jammer_range(atom/source)
+ for(var/obj/item/jammer/jammer as anything in GLOB.active_jammers)
+ if(IN_GIVEN_RANGE(source, jammer, jammer.range))
+ return TRUE
+ return FALSE
/obj/item/jammer
name = "radio jammer"
diff --git a/code/modules/admin/admin_pda_message.dm b/code/modules/admin/admin_pda_message.dm
index 2aeca2fd58c..44ca86f893e 100644
--- a/code/modules/admin/admin_pda_message.dm
+++ b/code/modules/admin/admin_pda_message.dm
@@ -13,7 +13,7 @@
if(!check_rights(R_ADMIN))
return
- if(!length(GLOB.TabletMessengers))
+ if(!length(GLOB.pda_messengers))
to_chat(usr, span_warning("ERROR: There are no users you can send a message to"))
return
@@ -22,15 +22,6 @@
/// Panel
/datum/admin_pda_panel
- /// PDA which have players(and we can send them message)
- var/available_pdas = list()
-
-/datum/admin_pda_panel/New()
- for(var/obj/item/modular_computer/computer in GLOB.TabletMessengers)
- for(var/datum/computer_file/program/messenger/app in computer.stored_files)
- if(!computer.saved_identification || !computer.saved_job || app.monitor_hidden || app.invisible)
- continue
- available_pdas += list(list("tablet" = computer, "name" = computer.saved_identification))
/datum/admin_pda_panel/ui_interact(mob/user, datum/tgui/ui)
ui = SStgui.try_update_ui(user, src, ui)
@@ -43,10 +34,15 @@
/datum/admin_pda_panel/ui_static_data(mob/user)
var/list/data = list()
- data["users"] = list()
-
- for(var/username in available_pdas)
- data["users"] += username["name"]
+ var/list/available_messengers = list()
+ for(var/messenger_ref in get_messengers_sorted_by_name())
+ var/datum/computer_file/program/messenger/messenger = GLOB.pda_messengers[messenger_ref]
+ available_messengers[REF(messenger)] = list(
+ ref = REF(messenger),
+ username = get_messenger_name(messenger),
+ invisible = messenger.invisible,
+ )
+ data["users"] = available_messengers
return data
/datum/admin_pda_panel/ui_act(action, list/params, datum/tgui/ui, datum/ui_state/state)
@@ -60,27 +56,45 @@
if("sendMessage")
var/targets = list()
var/spam = params["spam"]
- for(var/target in available_pdas)
- if(!spam && target["name"] != params["user"])
- continue
- targets += target["tablet"]
+ var/ref = params["ref"]
+ var/force = params["force"]
+ if(!spam && (ref in GLOB.pda_messengers))
+ targets += GLOB.pda_messengers[ref]
+ else
+ for(var/messenger_ref in get_messengers_sorted_by_name())
+ var/datum/computer_file/program/messenger/messenger = GLOB.pda_messengers[messenger_ref]
+ if(messenger.invisible && !params["include_invisible"])
+ continue
+ targets += messenger
if(!length(targets))
- to_chat(usr, span_warning("ERROR: Target is unavaiable(or not choosed)."))
- return
+ to_chat(usr, span_warning("ERROR: Target is unavailable."))
+ return FALSE
- var/datum/signal/subspace/messaging/tablet_msg/signal = new(targets[1], list(
- "name" = params["name"],
- "job" = params["job"],
- "message" = html_decode(params["message"]),
- "ref" = FALSE,
+ var/datum/signal/subspace/messaging/tablet_message/signal = new(null, list(
+ "fakename" = params["name"],
+ "fakejob" = params["job"],
+ "message" = params["message"],
+ "everyone" = spam,
+ "ref" = null,
"targets" = targets,
- "emojis" = FALSE,
"rigged" = FALSE,
- "photo" = FALSE,
"automated" = FALSE,
))
- signal.send_to_receivers()
- message_admins("[key_name_admin(usr)] has send custom PDA message to [spam ? "everyone" : params["user"]].")
- log_admin("[key_name(usr)] has send custom PDA message to [spam ? "everyone" : params["user"]]. Message: [params["message"]].")
+ if(force)
+ signal.broadcast()
+ else
+ signal.levels = SSmapping.levels_by_trait(ZTRAIT_STATION)
+ signal.send_to_receivers()
+
+ if(!(force || signal.data["reject"]))
+ to_chat(usr, span_warning("ERROR: PDA message was rejected by the telecomms setup."))
+ return FALSE
+
+ var/recipient = spam ? "everyone" : get_messenger_name(targets[1])
+
+ message_admins("[key_name_admin(usr)] sent a custom PDA message to [recipient].")
+ log_admin("[key_name(usr)] sent a custom PDA message to [recipient]. Message: [params["message"]].")
+ log_pda("[key_name(usr)] sent an admin custom PDA message to [recipient]. Message: [params["message"]]")
+ return TRUE
diff --git a/code/modules/clothing/chameleon/chameleon_action_subtypes.dm b/code/modules/clothing/chameleon/chameleon_action_subtypes.dm
index db8185c43ed..f619a429269 100644
--- a/code/modules/clothing/chameleon/chameleon_action_subtypes.dm
+++ b/code/modules/clothing/chameleon/chameleon_action_subtypes.dm
@@ -29,7 +29,7 @@
..()
var/obj/item/modular_computer/pda/agent_pda = target
if(istype(agent_pda) && istype(job_datum))
- agent_pda.saved_job = job_datum.title
+ agent_pda.imprint_id(job_name = job_datum.title)
/datum/action/item_action/chameleon/change/headset
chameleon_type = /obj/item/radio/headset
diff --git a/code/modules/clothing/outfits/ert.dm b/code/modules/clothing/outfits/ert.dm
index 4217c979398..e365c685546 100644
--- a/code/modules/clothing/outfits/ert.dm
+++ b/code/modules/clothing/outfits/ert.dm
@@ -168,8 +168,7 @@
return
var/obj/item/modular_computer/pda/heads/pda = H.r_store
- pda.saved_identification = H.real_name
- pda.saved_job = "CentCom Official"
+ pda.imprint_id(H.real_name, "CentCom Official")
var/obj/item/card/id/W = H.wear_id
W.registered_name = H.real_name
diff --git a/code/modules/clothing/outfits/standard.dm b/code/modules/clothing/outfits/standard.dm
index a130bc6d09d..15ede663374 100644
--- a/code/modules/clothing/outfits/standard.dm
+++ b/code/modules/clothing/outfits/standard.dm
@@ -217,8 +217,7 @@
sec_briefcase.contents += new /obj/item/grenade/c4/x4
var/obj/item/modular_computer/pda/heads/pda = H.belt
- pda.saved_identification = H.real_name
- pda.saved_job = "Reaper"
+ pda.imprint_id(H.real_name, "Reaper")
var/obj/item/card/id/W = H.wear_id
W.registered_name = H.real_name
diff --git a/code/modules/jobs/job_types/_job.dm b/code/modules/jobs/job_types/_job.dm
index 2031a90a28c..486eb7caff4 100644
--- a/code/modules/jobs/job_types/_job.dm
+++ b/code/modules/jobs/job_types/_job.dm
@@ -347,8 +347,7 @@
var/obj/item/modular_computer/pda/pda = equipped.get_item_by_slot(pda_slot)
if(istype(pda))
- pda.saved_identification = equipped.real_name
- pda.saved_job = equipped_job.title
+ pda.imprint_id(equipped.real_name, equipped_job.title)
pda.update_ringtone(equipped_job.job_tone)
pda.UpdateDisplay()
diff --git a/code/modules/jobs/job_types/security_officer.dm b/code/modules/jobs/job_types/security_officer.dm
index 60def9a09b6..72f96725387 100644
--- a/code/modules/jobs/job_types/security_officer.dm
+++ b/code/modules/jobs/job_types/security_officer.dm
@@ -161,16 +161,18 @@ GLOBAL_LIST_EMPTY(security_officer_distribution)
partners += partner.real_name
if (partners.len)
- for (var/obj/item/modular_computer/pda as anything in GLOB.TabletMessengers)
- if (pda.saved_identification in partners)
- targets += pda
+ for(var/messenger_ref in GLOB.pda_messengers)
+ var/datum/computer_file/program/messenger/messenger = GLOB.pda_messengers[messenger_ref]
+ if(!(messenger.computer?.saved_identification in partners))
+ continue
+ targets += messenger
if (!targets.len)
return
- var/datum/signal/subspace/messaging/tablet_msg/signal = new(announcement_system, list(
- "name" = "Security Department Update",
- "job" = "Automated Announcement System",
+ var/datum/signal/subspace/messaging/tablet_message/signal = new(announcement_system, list(
+ "fakename" = "Security Department Update",
+ "fakejob" = "Automated Announcement System",
"message" = "Officer [officer.real_name] has been assigned to your department, [department].",
"targets" = targets,
"automated" = TRUE,
diff --git a/code/modules/mob/living/silicon/ai/ai.dm b/code/modules/mob/living/silicon/ai/ai.dm
index a2f3fc35778..12a2b51f484 100644
--- a/code/modules/mob/living/silicon/ai/ai.dm
+++ b/code/modules/mob/living/silicon/ai/ai.dm
@@ -923,7 +923,7 @@
if(oldname != real_name)
if(eyeobj)
eyeobj.name = "[newname] (AI Eye)"
- modularInterface.saved_identification = real_name
+ modularInterface.imprint_id(name = real_name)
// Notify Cyborgs
for(var/mob/living/silicon/robot/Slave in connected_robots)
diff --git a/code/modules/mob/living/silicon/robot/robot.dm b/code/modules/mob/living/silicon/robot/robot.dm
index d773e49a003..6222d31881e 100644
--- a/code/modules/mob/living/silicon/robot/robot.dm
+++ b/code/modules/mob/living/silicon/robot/robot.dm
@@ -698,7 +698,7 @@
notify_ai(AI_NOTIFICATION_CYBORG_RENAMED, oldname, newname)
if(!QDELETED(builtInCamera))
builtInCamera.c_tag = real_name
- modularInterface.saved_identification = real_name
+ modularInterface.imprint_id(name = real_name)
custom_name = newname
diff --git a/code/modules/mob/living/silicon/robot/robot_defines.dm b/code/modules/mob/living/silicon/robot/robot_defines.dm
index a39bfb4b623..d414a55000e 100644
--- a/code/modules/mob/living/silicon/robot/robot_defines.dm
+++ b/code/modules/mob/living/silicon/robot/robot_defines.dm
@@ -213,8 +213,7 @@
/mob/living/silicon/robot/model/syndicate/create_modularInterface()
if(!modularInterface)
modularInterface = new /obj/item/modular_computer/pda/silicon/cyborg/syndicate(src)
- modularInterface.saved_identification = real_name
- modularInterface.saved_job = "Cyborg"
+ modularInterface.imprint_id(job_name = "Cyborg")
return ..()
/mob/living/silicon/robot/model/syndicate/proc/show_playstyle()
diff --git a/code/modules/mob/living/silicon/silicon.dm b/code/modules/mob/living/silicon/silicon.dm
index 16ebd6b3eaf..1b20b5d4b15 100644
--- a/code/modules/mob/living/silicon/silicon.dm
+++ b/code/modules/mob/living/silicon/silicon.dm
@@ -91,19 +91,20 @@
/mob/living/silicon/proc/create_modularInterface()
if(!modularInterface)
modularInterface = new /obj/item/modular_computer/pda/silicon(src)
+ var/job_name = ""
if(isAI(src))
- modularInterface.saved_job = "AI"
+ job_name = "AI"
if(ispAI(src))
- modularInterface.saved_job = "pAI Messenger"
+ job_name = "pAI Messenger"
modularInterface.layer = ABOVE_HUD_PLANE
SET_PLANE_EXPLICIT(modularInterface, ABOVE_HUD_PLANE, src)
- modularInterface.saved_identification = real_name || name
+ modularInterface.imprint_id(real_name || name, job_name)
/mob/living/silicon/robot/create_modularInterface()
if(!modularInterface)
modularInterface = new /obj/item/modular_computer/pda/silicon/cyborg(src)
- modularInterface.saved_job = "Cyborg"
+ modularInterface.imprint_id(job_name = "Cyborg")
return ..()
/mob/living/silicon/med_hud_set_health()
@@ -467,4 +468,4 @@
if(!modularInterface)
stack_trace("Silicon [src] ( [type] ) was somehow missing their integrated tablet. Please make a bug report.")
create_modularInterface()
- modularInterface.saved_identification = newname
+ modularInterface.imprint_id(name = newname)
diff --git a/code/modules/mob/mob.dm b/code/modules/mob/mob.dm
index f3f1b9b160f..51e6de5d49b 100644
--- a/code/modules/mob/mob.dm
+++ b/code/modules/mob/mob.dm
@@ -1218,7 +1218,7 @@
else if( search_pda && istype(A, /obj/item/modular_computer/pda) )
var/obj/item/modular_computer/pda/PDA = A
if(PDA.saved_identification == oldname)
- PDA.saved_identification = newname
+ PDA.imprint_id(name = newname)
PDA.UpdateDisplay()
if(!search_id)
break
diff --git a/code/modules/modular_computers/computers/item/computer.dm b/code/modules/modular_computers/computers/item/computer.dm
index a8e07b55f1a..18a17311c3f 100644
--- a/code/modules/modular_computers/computers/item/computer.dm
+++ b/code/modules/modular_computers/computers/item/computer.dm
@@ -1,5 +1,3 @@
-GLOBAL_LIST_EMPTY(TabletMessengers) // a list of all active messengers, similar to GLOB.PDAs (used primarily with ntmessenger.dm)
-
// This is the base type of computer
// Other types expand it - tablets and laptops are subtypes
// consoles use "procssor" item that is held inside it.
@@ -134,10 +132,9 @@ GLOBAL_LIST_EMPTY(TabletMessengers) // a list of all active messengers, similar
if(internal_cell)
internal_cell = new internal_cell(src)
- update_appearance()
- register_context()
- Add_Messenger()
install_default_programs()
+ register_context()
+ update_appearance()
/obj/item/modular_computer/proc/install_default_programs()
SHOULD_CALL_PARENT(FALSE)
@@ -151,7 +148,6 @@ GLOBAL_LIST_EMPTY(TabletMessengers) // a list of all active messengers, similar
//Some components will actually try and interact with this, so let's do it later
QDEL_NULL(soundloop)
QDEL_LIST(stored_files)
- Remove_Messenger()
if(istype(inserted_disk))
QDEL_NULL(inserted_disk)
@@ -647,6 +643,21 @@ GLOBAL_LIST_EMPTY(TabletMessengers) // a list of all active messengers, similar
enabled = FALSE
update_appearance()
+///Imprints name and job into the modular computer, and calls back to necessary functions.
+///Acts as a replacement to directly setting the imprints fields. All fields are optional, the proc will try to fill in missing gaps.
+/obj/item/modular_computer/proc/imprint_id(name = null, job_name = null)
+ saved_identification = name || computer_id_slot?.registered_name || saved_identification
+ saved_job = job_name || computer_id_slot?.assignment || saved_job
+ SEND_SIGNAL(src, COMSIG_MODULAR_PDA_IMPRINT_UPDATED, saved_identification, saved_job)
+ UpdateDisplay()
+
+///Resets the imprinted name and job back to null.
+/obj/item/modular_computer/proc/reset_imprint()
+ saved_identification = null
+ saved_job = null
+ SEND_SIGNAL(src, COMSIG_MODULAR_PDA_IMPRINT_RESET)
+ UpdateDisplay()
+
/obj/item/modular_computer/ui_action_click(mob/user, actiontype)
if(istype(actiontype, /datum/action/item_action/toggle_computer_light))
toggle_flashlight()
@@ -722,6 +733,14 @@ GLOBAL_LIST_EMPTY(TabletMessengers) // a list of all active messengers, similar
to_chat(user, span_notice("You plug \the [attacking_item] to \the [src]."))
return
+ if(istype(attacking_item, /obj/item/photo))
+ var/obj/item/photo/attacking_photo = attacking_item
+ if(store_file(new /datum/computer_file/picture(attacking_photo.picture)))
+ balloon_alert(user, "photo scanned")
+ else
+ balloon_alert(user, "no space!")
+ return
+
// Check if any Applications need it
for(var/datum/computer_file/item_holding_app as anything in stored_files)
if(item_holding_app.application_attackby(attacking_item, user))
@@ -818,12 +837,6 @@ GLOBAL_LIST_EMPTY(TabletMessengers) // a list of all active messengers, similar
return physical.Adjacent(neighbor)
return ..()
-/obj/item/modular_computer/proc/Add_Messenger()
- GLOB.TabletMessengers += src
-
-/obj/item/modular_computer/proc/Remove_Messenger()
- GLOB.TabletMessengers -= src
-
///Returns a string of what to send at the end of messenger's messages.
/obj/item/modular_computer/proc/get_messenger_ending()
return "Sent from my PDA"
diff --git a/code/modules/modular_computers/computers/item/computer_files.dm b/code/modules/modular_computers/computers/item/computer_files.dm
index 296f5be0992..6b6fbd179f2 100644
--- a/code/modules/modular_computers/computers/item/computer_files.dm
+++ b/code/modules/modular_computers/computers/item/computer_files.dm
@@ -16,7 +16,8 @@
file_storing.computer = src
used_capacity += file_storing.size
- SEND_SIGNAL(file_storing, COMSIG_MODULAR_COMPUTER_FILE_STORE, src)
+ SEND_SIGNAL(file_storing, COMSIG_COMPUTER_FILE_STORE, src)
+ SEND_SIGNAL(src, COMSIG_MODULAR_COMPUTER_FILE_STORE, file_storing)
return TRUE
/**
@@ -37,7 +38,8 @@
stored_files.Remove(file_removing)
used_capacity -= file_removing.size
- SEND_SIGNAL(file_removing, COMSIG_MODULAR_COMPUTER_FILE_DELETED)
+ SEND_SIGNAL(src, COMSIG_MODULAR_COMPUTER_FILE_DELETE, file_removing)
+ SEND_SIGNAL(file_removing, COMSIG_COMPUTER_FILE_DELETE)
qdel(file_removing)
return TRUE
@@ -73,14 +75,34 @@
* If a computer disk is passed instead, it will check the disk over the computer.
*/
/obj/item/modular_computer/proc/find_file_by_name(filename, obj/item/computer_disk/target_disk)
- if(!filename)
+ if(!istext(filename))
return null
- if(target_disk)
- for(var/datum/computer_file/file as anything in target_disk.stored_files)
- if(file.filename == filename)
- return file
- else
+ if(isnull(target_disk))
for(var/datum/computer_file/file as anything in stored_files)
if(file.filename == filename)
return file
+ else
+ for(var/datum/computer_file/file as anything in target_disk.stored_files)
+ if(file.filename == filename)
+ return file
+ return null
+
+/**
+ * find_file_by_uid
+ *
+ * Will check all files in this computer and returns the file with the matching uid.
+ * A file's uid is always unique to them, so this proc is sometimes preferable over find_file_by_name.
+ * If a computer disk is passed instead, it will check the disk over the computer.
+ */
+/obj/item/modular_computer/proc/find_file_by_uid(uid, obj/item/computer_disk/target_disk)
+ if(!isnum(uid))
+ return null
+ if(isnull(target_disk))
+ for(var/datum/computer_file/file as anything in stored_files)
+ if(file.uid == uid)
+ return file
+ else
+ for(var/datum/computer_file/file as anything in target_disk.stored_files)
+ if(file.uid == uid)
+ return file
return null
diff --git a/code/modules/modular_computers/computers/item/computer_ui.dm b/code/modules/modular_computers/computers/item/computer_ui.dm
index 2a76a20ef1b..fa00efae0a2 100644
--- a/code/modules/modular_computers/computers/item/computer_ui.dm
+++ b/code/modules/modular_computers/computers/item/computer_ui.dm
@@ -15,6 +15,7 @@
if(!active_ui)
if(active_program)
active_ui = new(user, src, active_program.tgui_id, active_program.filedesc)
+ active_program.ui_interact(user, active_ui)
else
active_ui = new(user, src, "NtosMain")
return active_ui.open()
@@ -22,6 +23,7 @@
if(active_program)
active_ui.interface = active_program.tgui_id
active_ui.title = active_program.filedesc
+ active_program.ui_interact(user, active_ui)
else
active_ui.interface = "NtosMain"
@@ -197,8 +199,7 @@
return TRUE
if("PC_Imprint_ID")
- saved_identification = computer_id_slot.registered_name
- saved_job = computer_id_slot.assignment
+ imprint_id()
UpdateDisplay()
playsound(src, 'sound/machines/terminal_processing.ogg', 15, TRUE)
diff --git a/code/modules/modular_computers/computers/item/disks/virus_disk.dm b/code/modules/modular_computers/computers/item/disks/virus_disk.dm
index d566c1f6f44..e3eac7736f5 100644
--- a/code/modules/modular_computers/computers/item/disks/virus_disk.dm
+++ b/code/modules/modular_computers/computers/item/disks/virus_disk.dm
@@ -9,7 +9,7 @@
///How many charges the virus has left
var/charges = 5
-/obj/item/computer_disk/virus/proc/send_virus(obj/item/modular_computer/pda/source, obj/item/modular_computer/pda/target, mob/living/user)
+/obj/item/computer_disk/virus/proc/send_virus(obj/item/modular_computer/pda/source, obj/item/modular_computer/pda/target, mob/living/user, message)
if(charges <= 0)
to_chat(user, span_notice("ERROR: Out of charges."))
return FALSE
@@ -26,7 +26,7 @@
/obj/item/computer_disk/virus/clown
name = "\improper H.O.N.K. disk"
-/obj/item/computer_disk/virus/clown/send_virus(obj/item/modular_computer/pda/source, obj/item/modular_computer/pda/target, mob/living/user)
+/obj/item/computer_disk/virus/clown/send_virus(obj/item/modular_computer/pda/source, obj/item/modular_computer/pda/target, mob/living/user, message)
. = ..()
if(!.)
return FALSE
@@ -43,7 +43,7 @@
/obj/item/computer_disk/virus/mime
name = "\improper sound of silence disk"
-/obj/item/computer_disk/virus/mime/send_virus(obj/item/modular_computer/pda/source, obj/item/modular_computer/pda/target, mob/living/user)
+/obj/item/computer_disk/virus/mime/send_virus(obj/item/modular_computer/pda/source, obj/item/modular_computer/pda/target, mob/living/user, message)
. = ..()
if(!.)
return FALSE
@@ -53,7 +53,7 @@
return FALSE
user.show_message(span_notice("Success!"))
charges--
- app.ringer_status = FALSE
+ app.alert_silenced = TRUE
app.ringtone = ""
/**
@@ -64,7 +64,7 @@
name = "\improper D.E.T.O.M.A.T.I.X. disk"
charges = 6
-/obj/item/computer_disk/virus/detomatix/send_virus(obj/item/modular_computer/pda/source, obj/item/modular_computer/pda/target, mob/living/user)
+/obj/item/computer_disk/virus/detomatix/send_virus(obj/item/modular_computer/pda/source, obj/item/modular_computer/pda/target, mob/living/user, message)
. = ..()
if(!.)
return FALSE
@@ -82,15 +82,18 @@
var/fakejob = sanitize_name(tgui_input_text(user, "Enter a job for the rigged message.", "Forge Message", max_length = MAX_NAME_LEN), allow_numbers = TRUE)
if(!fakejob || source != original_host || !user.can_perform_action(source))
return
+ var/attach_fake_photo = tgui_alert(user, "Attach a fake photo?", "Forge Message", list("Yes", "No")) == "Yes"
var/datum/computer_file/program/messenger/app = locate() in source.stored_files
- if(!app || charges <= 0 || !app.send_message(user, list(target), rigged = REF(user), fake_name = fakename, fake_job = fakejob))
+ var/datum/computer_file/program/messenger/target_app = locate() in target.stored_files
+ if(!app || charges <= 0 || !app.send_rigged_message(user, message, list(target_app), fakename, fakejob, attach_fake_photo))
return FALSE
charges--
user.show_message(span_notice("Success!"))
var/reference = REF(src)
target.add_traits(list(TRAIT_PDA_CAN_EXPLODE, TRAIT_PDA_MESSAGE_MENU_RIGGED), reference)
addtimer(TRAIT_CALLBACK_REMOVE(target, TRAIT_PDA_MESSAGE_MENU_RIGGED, reference), 10 SECONDS)
+ addtimer(TRAIT_CALLBACK_REMOVE(target, TRAIT_PDA_CAN_EXPLODE, reference), 1 MINUTES)
return TRUE
/**
@@ -119,7 +122,7 @@
telecrystal_stack.use(telecrystal_stack.amount)
-/obj/item/computer_disk/virus/frame/send_virus(obj/item/modular_computer/pda/source, obj/item/modular_computer/pda/target, mob/living/user)
+/obj/item/computer_disk/virus/frame/send_virus(obj/item/modular_computer/pda/source, obj/item/modular_computer/pda/target, mob/living/user, message)
. = ..()
if(!.)
return FALSE
diff --git a/code/modules/modular_computers/computers/item/role_tablet_presets.dm b/code/modules/modular_computers/computers/item/role_tablet_presets.dm
index f4d982ed35b..a74114dacd2 100644
--- a/code/modules/modular_computers/computers/item/role_tablet_presets.dm
+++ b/code/modules/modular_computers/computers/item/role_tablet_presets.dm
@@ -362,7 +362,7 @@
. = ..()
for(var/datum/computer_file/program/messenger/msg in stored_files)
msg.mime_mode = TRUE
- msg.ringer_status = FALSE
+ msg.alert_silenced = TRUE
/obj/item/modular_computer/pda/curator
name = "curator PDA"
@@ -380,7 +380,7 @@
/obj/item/modular_computer/pda/curator/Initialize(mapload)
. = ..()
for(var/datum/computer_file/program/messenger/msg in stored_files)
- msg.ringer_status = FALSE
+ msg.alert_silenced = TRUE
/obj/item/modular_computer/pda/psychologist
name = "psychologist PDA"
diff --git a/code/modules/modular_computers/file_system/computer_file.dm b/code/modules/modular_computers/file_system/computer_file.dm
index b26df2bf475..99fc288ffaf 100644
--- a/code/modules/modular_computers/file_system/computer_file.dm
+++ b/code/modules/modular_computers/file_system/computer_file.dm
@@ -19,7 +19,7 @@
/datum/computer_file/New()
..()
uid = file_uid++
- RegisterSignal(src, COMSIG_MODULAR_COMPUTER_FILE_STORE, PROC_REF(on_install))
+ RegisterSignal(src, COMSIG_COMPUTER_FILE_STORE, PROC_REF(on_install))
/datum/computer_file/Destroy(force)
if(computer)
diff --git a/code/modules/modular_computers/file_system/picture_file.dm b/code/modules/modular_computers/file_system/picture_file.dm
new file mode 100644
index 00000000000..4ba71aee3d7
--- /dev/null
+++ b/code/modules/modular_computers/file_system/picture_file.dm
@@ -0,0 +1,37 @@
+/**
+ * PNG file type
+ * Stores a picture which can be used by other programs.
+ */
+/datum/computer_file/picture
+ filetype = "PNG" // the superior filetype
+ size = 1
+ /// The instance of the stored picture.
+ var/datum/picture/stored_picture
+ /// The name of the asset cache item.
+ /// This will be initialized after assign_path() is called.
+ var/picture_name
+
+/datum/computer_file/picture/New(datum/picture/stored_picture, picture_name)
+ ..()
+ if(isnull(stored_picture))
+ return
+ src.filename = "[stored_picture.picture_name] ([uid])"
+ src.stored_picture = stored_picture
+ src.picture_name = picture_name
+
+/datum/computer_file/picture/on_install(datum/computer_file/source, obj/item/modular_computer/computer_installing)
+ . = ..()
+ assign_path()
+
+/// Assigns an asset path to the stored image, for use in the UI.
+/datum/computer_file/picture/proc/assign_path()
+ if(!isnull(picture_name))
+ return
+ picture_name = SSmodular_computers.get_next_picture_name()
+ SSassets.transport.register_asset(picture_name, stored_picture.picture_image)
+
+/datum/computer_file/picture/clone(rename = FALSE)
+ var/datum/computer_file/picture/temp = ..()
+ temp.stored_picture = stored_picture
+ temp.picture_name = picture_name
+ return temp
diff --git a/code/modules/modular_computers/file_system/program.dm b/code/modules/modular_computers/file_system/program.dm
index 2bf9a04a176..c8034335af4 100644
--- a/code/modules/modular_computers/file_system/program.dm
+++ b/code/modules/modular_computers/file_system/program.dm
@@ -51,6 +51,14 @@
temp.usage_flags = usage_flags
return temp
+/**
+ * WARNING: this proc does not work the same as normal `ui_interact`, as the
+ * computer takes care of opening the UI. The `datum/tgui/ui` parameter will always exist.
+ * This proc only serves as a callback.
+ */
+/datum/computer_file/program/ui_interact(mob/user, datum/tgui/ui)
+ SHOULD_CALL_PARENT(FALSE)
+
///We are not calling parent as it's handled by the computer itself, this is only called after.
/datum/computer_file/program/ui_act(action, params, datum/tgui/ui, datum/ui_state/state)
SHOULD_CALL_PARENT(FALSE)
diff --git a/code/modules/modular_computers/file_system/programs/frontier.dm b/code/modules/modular_computers/file_system/programs/frontier.dm
index 778fbab578e..aa897467a8c 100644
--- a/code/modules/modular_computers/file_system/programs/frontier.dm
+++ b/code/modules/modular_computers/file_system/programs/frontier.dm
@@ -38,7 +38,7 @@
/datum/computer_file/program/scipaper_program/proc/recheck_file_presence()
if(selected_file in computer.stored_files)
return FALSE
- UnregisterSignal(selected_file, COMSIG_MODULAR_COMPUTER_FILE_DELETED)
+ UnregisterSignal(selected_file, COMSIG_COMPUTER_FILE_DELETE)
selected_file = null
paper_to_be.set_experiment()
@@ -189,12 +189,12 @@
return TRUE
if("select_file") // Selecting new file will necessitate a change in paper type. This will be done on select_experiment and not here.
if(selected_file)
- UnregisterSignal(selected_file, COMSIG_MODULAR_COMPUTER_FILE_DELETED)
+ UnregisterSignal(selected_file, COMSIG_COMPUTER_FILE_DELETE)
paper_to_be.set_experiment() // Clears the paper info.
for(var/datum/computer_file/data/ordnance/ordnance_data in computer.stored_files)
if(ordnance_data.uid == params["selected_uid"])
selected_file = ordnance_data
- RegisterSignal(selected_file, COMSIG_MODULAR_COMPUTER_FILE_DELETED, PROC_REF(recheck_file_presence))
+ RegisterSignal(selected_file, COMSIG_COMPUTER_FILE_DELETE, PROC_REF(recheck_file_presence))
return TRUE
if("select_experiment")
var/ex_path = text2path(params["selected_expath"])
@@ -229,7 +229,7 @@
if(linked_techweb.add_scientific_paper(paper_to_be))
computer.say("\"[paper_to_be.title]\" has been published!")
paper_to_be = new
- UnregisterSignal(selected_file, COMSIG_MODULAR_COMPUTER_FILE_DELETED)
+ UnregisterSignal(selected_file, COMSIG_COMPUTER_FILE_DELETE)
selected_file = null
SStgui.update_uis(src)
playsound(computer, 'sound/machines/ping.ogg', 25)
diff --git a/code/modules/modular_computers/file_system/programs/messenger/messenger_data.dm b/code/modules/modular_computers/file_system/programs/messenger/messenger_data.dm
new file mode 100644
index 00000000000..0b1514d4c04
--- /dev/null
+++ b/code/modules/modular_computers/file_system/programs/messenger/messenger_data.dm
@@ -0,0 +1,152 @@
+/// A list of all active and visible messengers
+GLOBAL_LIST_EMPTY_TYPED(pda_messengers, /datum/computer_file/program/messenger)
+
+/// Registers an NTMessenger instance to the list of pda_messengers.
+/proc/add_messenger(datum/computer_file/program/messenger/messenger)
+ var/obj/item/modular_computer/messenger_device = messenger.computer
+ // a bunch of empty PDAs are normally allocated, we don't want that clutter
+ if(!messenger_device.saved_identification || !messenger_device.saved_job)
+ return
+
+ if(!istype(messenger))
+ return
+
+ var/messenger_ref = REF(messenger)
+ if(messenger_ref in GLOB.pda_messengers)
+ return
+
+ GLOB.pda_messengers[messenger_ref] = messenger
+
+/// Unregisters an NTMessenger instance from the pda_messengers table.
+/proc/remove_messenger(datum/computer_file/program/messenger/messenger)
+ if(!istype(messenger))
+ return
+
+ var/messenger_ref = REF(messenger)
+ if(!(messenger_ref in GLOB.pda_messengers))
+ return
+
+ GLOB.pda_messengers.Remove(messenger_ref)
+
+/// Gets all messengers, sorted by their name
+/proc/get_messengers_sorted_by_name()
+ return sortTim(GLOB.pda_messengers.Copy(), GLOBAL_PROC_REF(cmp_pdaname_asc), associative = TRUE)
+
+/// Gets all messengers, sorted by their job
+/proc/get_messengers_sorted_by_job()
+ return sortTim(GLOB.pda_messengers.Copy(), GLOBAL_PROC_REF(cmp_pdajob_asc), associative = TRUE)
+
+/// Get the display name of a messenger instance
+/proc/get_messenger_name(datum/computer_file/program/messenger/messenger)
+ if(!istype(messenger))
+ return null
+ var/obj/item/modular_computer/computer = messenger.computer
+ if(!istype(computer))
+ return null
+ return STRINGIFY_PDA_TARGET(computer.saved_identification, computer.saved_job)
+
+/**
+ * Chat log data type, stores information about the recipient,
+ * the messages themselves and other metadata.
+ */
+/datum/pda_chat
+ /// The cached name of the recipient, so we can
+ /// identify this chat even after the recipient is deleted
+ var/cached_name = "Unknown"
+ /// The cached job of the recipient
+ var/cached_job = "Unknown"
+ /// Weakref to the recipient messenger
+ var/datum/weakref/recipient = null
+ /// A list of messages in this chat
+ var/list/datum/pda_message/messages = list()
+ /// Used to determine if we should show this in recents
+ var/visible_in_recents = FALSE
+ /// Used to determine if you can talk in a chat
+ var/can_reply = TRUE
+ /// Saved draft of a message so the sender can leave and come back later
+ var/message_draft = ""
+ /// Number of unread messages in this chat
+ var/unread_messages = 0
+
+/datum/pda_chat/New(datum/computer_file/program/messenger/recipient)
+ src.recipient = WEAKREF(recipient)
+ src.can_reply = !isnull(recipient)
+
+/// Adds a message to the chat log and optionally shows the chat in recents.
+/// Call this instead of adding to messages directly.
+/datum/pda_chat/proc/add_message(datum/pda_message/message, show_in_recents = TRUE)
+ messages += message
+ if(!visible_in_recents && show_in_recents)
+ visible_in_recents = TRUE
+ return message
+
+/// Returns this datum as an associative list, used for ui_data calls.
+/datum/pda_chat/proc/get_ui_data(mob/user)
+ var/list/data = list()
+
+ var/list/recipient_data = list()
+
+ recipient_data["name"] = get_recipient_name()
+ recipient_data["job"] = get_recipient_job()
+ recipient_data["ref"] = recipient?.reference
+
+ data["ref"] = REF(src)
+ data["recipient"] = recipient_data
+
+ var/list/messages_data = list()
+ for(var/datum/pda_message/message as anything in messages)
+ messages_data += list(message.get_ui_data(user))
+ data["messages"] = messages_data
+ data["message_draft"] = message_draft
+
+ data["visible"] = visible_in_recents
+ data["can_reply"] = can_reply
+ data["unread_messages"] = unread_messages
+
+ return data
+
+/// Returns the messenger's name, caches the name in case the recipient becomes invalid later.
+/datum/pda_chat/proc/get_recipient_name()
+ var/datum/computer_file/program/messenger/messenger = recipient?.resolve()
+ if(istype(messenger) && (recipient.reference in GLOB.pda_messengers))
+ cached_name = messenger.computer.saved_identification
+ return cached_name
+
+/// Returns the messenger's job, caches the job in case the recipient becomes invalid later.
+/datum/pda_chat/proc/get_recipient_job()
+ var/datum/computer_file/program/messenger/messenger = recipient?.resolve()
+ if(istype(messenger) && (recipient.reference in GLOB.pda_messengers))
+ cached_job = messenger.computer.saved_job
+ return cached_job
+
+/**
+ * Chat message data type, stores data about messages themselves.
+ */
+/datum/pda_message
+ /// The message itself.
+ var/message
+ /// Whether the message is sent by the user or not.
+ var/outgoing
+ /// The name of the photo asset in the SSassets cache, the URL of which is sent to the client.
+ var/photo_name
+ /// Whether this message was sent to everyone.
+ var/everyone
+ /// The station time at which this message was made.
+ var/timestamp
+
+/datum/pda_message/New(message, outgoing, timestamp, photo_name = null, everyone = FALSE)
+ src.message = message
+ src.outgoing = outgoing
+ src.timestamp = timestamp
+ src.photo_name = photo_name
+ src.everyone = everyone
+
+/// Returns an associative list of the message's data, used for ui_data calls.
+/datum/pda_message/proc/get_ui_data(mob/user)
+ var/list/data = list()
+ data["message"] = message
+ data["outgoing"] = outgoing
+ data["photo_path"] = photo_name ? SSassets.transport.get_asset_url(photo_name) : null
+ data["everyone"] = everyone
+ data["timestamp"] = timestamp
+ return data
diff --git a/code/modules/modular_computers/file_system/programs/messenger/messenger_program.dm b/code/modules/modular_computers/file_system/programs/messenger/messenger_program.dm
new file mode 100644
index 00000000000..06eb507167c
--- /dev/null
+++ b/code/modules/modular_computers/file_system/programs/messenger/messenger_program.dm
@@ -0,0 +1,731 @@
+/// Used to generate an asset key for a temporary image unique to this user.
+#define TEMP_IMAGE_PATH(ref) ("ntos_messenger[ref]_temp_image.png")
+/// Purpose is evident by the name, hopefully.
+#define MAX_PDA_MESSAGE_LEN 1024
+/// Format of message timestamps.
+#define PDA_MESSAGE_TIMESTAMP_FORMAT "hh:mm"
+
+/datum/computer_file/program/messenger
+ filename = "nt_messenger"
+ filedesc = "Direct Messenger"
+ category = PROGRAM_CATEGORY_MISC
+ program_icon_state = "command"
+ extended_desc = "This program allows old-school communication with other modular devices."
+ size = 0
+ undeletable = TRUE // It comes by default in tablets, can't be downloaded, takes no space and should obviously not be able to be deleted.
+ header_program = TRUE
+ available_on_ntnet = FALSE
+ usage_flags = PROGRAM_TABLET
+ ui_header = "ntnrc_idle.gif"
+ tgui_id = "NtosMessenger"
+ program_icon = "comment-alt"
+ alert_able = TRUE
+
+ /// Whether the user is invisible to the message list.
+ var/invisible = FALSE
+ /// great wisdom from PDA.dm - "no spamming" (prevents people from spamming the same message over and over)
+ COOLDOWN_DECLARE(last_text)
+ /// even more wisdom from PDA.dm - "no everyone spamming" (prevents people from spamming the same message over and over)
+ COOLDOWN_DECLARE(last_text_everyone)
+ /// Whether or not we're in a mime PDA.
+ var/mime_mode = FALSE
+ /// Whether this app can send messages to all.
+ var/spam_mode = FALSE
+
+ /// An asssociative list of chats we have started, format: chatref -> pda_chat.
+ var/list/saved_chats = list()
+ /// Whose chatlogs we currently have open. If we are in the contacts list, this is null.
+ var/viewing_messages_of = null
+
+ /// The current ringtone (displayed in the chat when a message is received).
+ var/ringtone = MESSENGER_RINGTONE_DEFAULT
+ /// Whether or not we're sorting by job.
+ var/sort_by_job = TRUE
+ /// Whether or not we're sending and receiving messages.
+ var/sending_and_receiving = TRUE
+ /// Selected photo for sending purposes.
+ var/selected_image = null
+ /// Whether or not we're sending (or trying to send) a virus.
+ var/sending_virus = FALSE
+
+/datum/computer_file/program/messenger/on_install()
+ . = ..()
+ RegisterSignal(computer, COMSIG_MODULAR_COMPUTER_FILE_STORE, PROC_REF(check_new_photo))
+ RegisterSignal(computer, COMSIG_MODULAR_COMPUTER_FILE_DELETE, PROC_REF(check_photo_removed))
+ RegisterSignal(computer, COMSIG_MODULAR_PDA_IMPRINT_UPDATED, PROC_REF(on_imprint_added))
+ RegisterSignal(computer, COMSIG_MODULAR_PDA_IMPRINT_RESET, PROC_REF(on_imprint_reset))
+
+/datum/computer_file/program/messenger/proc/check_new_photo(sender, datum/computer_file/picture/storing_picture)
+ SIGNAL_HANDLER
+ if(!istype(storing_picture))
+ return
+ update_pictures_for_all()
+
+/datum/computer_file/program/messenger/proc/check_photo_removed(sender, datum/computer_file/picture/photo_removed)
+ SIGNAL_HANDLER
+ if(istype(photo_removed) && selected_image == photo_removed.picture_name)
+ selected_image = null
+
+/datum/computer_file/program/messenger/proc/on_imprint_added(sender)
+ SIGNAL_HANDLER
+ add_messenger(src)
+
+/datum/computer_file/program/messenger/proc/on_imprint_reset(sender)
+ SIGNAL_HANDLER
+ remove_messenger(src)
+ saved_chats = list()
+ selected_image = null
+ viewing_messages_of = null
+
+/datum/computer_file/program/messenger/Destroy(force)
+ if(!QDELETED(computer))
+ stack_trace("Attempted to qdel messenger of [computer] without qdeling computer, this will cause problems later")
+ remove_messenger(src)
+ return ..()
+
+/// Gets the list of available messengers
+/datum/computer_file/program/messenger/proc/get_messengers()
+ var/list/dictionary = list()
+
+ var/list/messengers_sorted = sort_by_job ? get_messengers_sorted_by_job() : get_messengers_sorted_by_name()
+
+ for(var/messenger_ref in messengers_sorted)
+ var/datum/computer_file/program/messenger/messenger = messengers_sorted[messenger_ref]
+ if(messenger == src || messenger.invisible)
+ continue
+
+ var/list/data = list()
+ data["name"] = messenger.computer.saved_identification
+ data["job"] = messenger.computer.saved_job
+ data["ref"] = REF(messenger)
+
+ dictionary[data["ref"]] = data
+
+ return dictionary
+
+/// Checks if the person can send an everyone message
+/datum/computer_file/program/messenger/proc/can_send_everyone_message()
+ return COOLDOWN_FINISHED(src, last_text) && COOLDOWN_FINISHED(src, last_text_everyone)
+
+/// Gets all currently relevant photo asset keys
+/datum/computer_file/program/messenger/proc/get_picture_assets()
+ var/list/data = list()
+
+ for(var/datum/computer_file/picture/photo in computer.stored_files)
+ data |= photo.picture_name
+
+ if(viewing_messages_of in saved_chats)
+ var/datum/pda_chat/chat = saved_chats[viewing_messages_of]
+ for(var/datum/pda_message/message as anything in chat.messages)
+ if(isnull(message.photo_name))
+ continue
+ data |= message.photo_name
+
+ if(!isnull(selected_image))
+ data |= selected_image
+
+ return data
+
+/// Sends new datum/picture assets to everyone
+/datum/computer_file/program/messenger/proc/update_pictures_for_all()
+ var/list/data = get_picture_assets()
+
+ if(isnull(computer.open_uis))
+ return
+
+ for(var/datum/tgui/window as anything in computer.open_uis)
+ SSassets.transport.send_assets(window.user, data)
+
+/datum/computer_file/program/messenger/ui_interact(mob/user, datum/tgui/ui)
+ var/list/data = get_picture_assets()
+ SSassets.transport.send_assets(user, data)
+
+/datum/computer_file/program/messenger/ui_state(mob/user)
+ if(issilicon(user))
+ return GLOB.reverse_contained_state
+ return GLOB.default_state
+
+/datum/computer_file/program/messenger/ui_act(action, list/params, datum/tgui/ui)
+ switch(action)
+ if("PDA_ringSet")
+ var/new_ringtone = tgui_input_text(usr, "Enter a new ringtone", "Ringtone", ringtone, MESSENGER_RINGTONE_MAX_LENGTH)
+ var/mob/living/usr_mob = usr
+ if(!new_ringtone || !in_range(computer, usr_mob) || computer.loc != usr_mob)
+ return FALSE
+
+ if(SEND_SIGNAL(computer, COMSIG_TABLET_CHANGE_ID, usr_mob, new_ringtone) & COMPONENT_STOP_RINGTONE_CHANGE)
+ return FALSE
+
+ ringtone = new_ringtone
+ return TRUE
+
+ if("PDA_toggleAlerts")
+ alert_silenced = !alert_silenced
+ return TRUE
+
+ if("PDA_toggleSendingAndReceiving")
+ sending_and_receiving = !sending_and_receiving
+ return TRUE
+
+ if("PDA_viewMessages")
+ if(viewing_messages_of in saved_chats)
+ var/datum/pda_chat/chat = saved_chats[viewing_messages_of]
+ chat.unread_messages = 0
+
+ viewing_messages_of = params["ref"]
+
+ if (viewing_messages_of in saved_chats)
+ var/datum/pda_chat/chat = saved_chats[viewing_messages_of]
+ chat.visible_in_recents = TRUE
+
+ selected_image = null
+ return TRUE
+
+ if("PDA_closeMessages")
+ var/target = params["ref"]
+
+ if(!(target in saved_chats))
+ return FALSE
+
+ var/datum/pda_chat/chat = saved_chats[target]
+ chat.visible_in_recents = FALSE
+ if(viewing_messages_of == target)
+ viewing_messages_of = null
+ return TRUE
+
+ if("PDA_clearMessages")
+ var/chat_ref = params["ref"]
+
+ if(chat_ref in saved_chats)
+ saved_chats.Remove(chat_ref)
+ else if(isnull(chat_ref))
+ saved_chats = list()
+
+ viewing_messages_of = null
+ return TRUE
+
+ if("PDA_changeSortStyle")
+ sort_by_job = !sort_by_job
+ return TRUE
+
+ if("PDA_sendEveryone")
+ if(!sending_and_receiving)
+ to_chat(usr, span_notice("ERROR: This device has sending disabled."))
+ return FALSE
+
+ if(!spam_mode)
+ to_chat(usr, span_notice("ERROR: This device does not have mass-messaging perms."))
+ return FALSE
+
+ if(!can_send_everyone_message())
+ return FALSE
+
+ return send_message_to_all(usr, params["message"])
+
+ if("PDA_saveMessageDraft")
+ var/target_chat_ref = params["ref"]
+ var/message_draft = params["message"]
+
+ if(!(target_chat_ref in saved_chats))
+ return FALSE
+
+ var/datum/pda_chat/chat = saved_chats[target_chat_ref]
+
+ chat.message_draft = message_draft
+
+ return TRUE
+
+ if("PDA_clearUnreads")
+ var/target_chat_ref = params["ref"]
+
+ if(!(target_chat_ref in saved_chats))
+ return FALSE
+
+ var/datum/pda_chat/chat = saved_chats[target_chat_ref]
+ chat.unread_messages = 0
+
+ return TRUE
+
+ if("PDA_sendMessage")
+ if(!sending_and_receiving)
+ to_chat(usr, span_notice("ERROR: This device has sending disabled."))
+ return FALSE
+
+ // target ref, can either be a chat in saved_chats
+ // or a messenger ref in GLOB.pda_messengers
+ var/target_ref = params["ref"]
+
+ var/target = null
+
+ if(target_ref in saved_chats)
+ target = saved_chats[target_ref]
+ else if(target_ref in GLOB.pda_messengers)
+ target = GLOB.pda_messengers[target_ref]
+ else
+ return FALSE
+
+ if(sending_virus)
+ var/obj/item/computer_disk/virus/disk = computer.inserted_disk
+ if(!istype(disk))
+ return FALSE
+
+ var/datum/computer_file/program/messenger/target_messenger = null
+
+ if(istype(target, /datum/pda_chat))
+ var/datum/pda_chat/target_chat = target
+ target_messenger = target_chat.recipient?.resolve()
+ if(!istype(target_messenger))
+ to_chat(usr, span_notice("ERROR: Recipient no longer exists."))
+ return FALSE
+ else if(istype(target, /datum/computer_file/program/messenger))
+ target_messenger = target
+
+ return disk.send_virus(computer, target_messenger.computer, usr, params["message"])
+
+ return send_message(usr, params["message"], list(target))
+
+ if("PDA_clearPhoto")
+ selected_image = null
+ return TRUE
+
+ if("PDA_toggleVirus")
+ sending_virus = !sending_virus
+ return TRUE
+
+ if("PDA_selectPhoto")
+ if(issilicon(usr))
+ return FALSE
+
+ var/photo_uid = text2num(params["uid"])
+
+ var/datum/computer_file/picture/selected_photo = computer.find_file_by_uid(photo_uid)
+
+ if(!istype(selected_photo))
+ return FALSE
+
+ selected_image = selected_photo.picture_name
+ return TRUE
+
+ if("PDA_siliconSelectPhoto")
+ if(!issilicon(usr))
+ return FALSE
+ var/mob/living/silicon/user = usr
+ if(!user.aicamera)
+ return FALSE
+ var/datum/picture/selected_photo = user.aicamera.selectpicture(user)
+ if(!selected_photo)
+ return FALSE
+ SSassets.transport.register_asset(TEMP_IMAGE_PATH(REF(src)), selected_photo.picture_image)
+ selected_image = TEMP_IMAGE_PATH(REF(src))
+ update_pictures_for_all()
+ return TRUE
+
+/datum/computer_file/program/messenger/ui_static_data(mob/user)
+ var/list/static_data = list()
+
+ static_data["can_spam"] = spam_mode
+ static_data["is_silicon"] = issilicon(user)
+ static_data["alert_able"] = alert_able
+
+ return static_data
+
+/datum/computer_file/program/messenger/ui_data(mob/user)
+ var/list/data = list()
+
+ var/list/chats_data = list()
+ for(var/chat_ref in saved_chats)
+ var/datum/pda_chat/chat = saved_chats[chat_ref]
+ var/list/chat_data = chat.get_ui_data(user)
+ chats_data[chat_ref] = chat_data
+
+ var/list/messengers = get_messengers()
+
+ data["owner"] = ((REF(src) in GLOB.pda_messengers) ? list(
+ "name" = computer.saved_identification,
+ "job" = computer.saved_job,
+ "ref" = REF(src)
+ ) : null)
+ data["saved_chats"] = chats_data
+ data["messengers"] = messengers
+ data["sort_by_job"] = sort_by_job
+ data["alert_silenced"] = alert_silenced
+ data["sending_and_receiving"] = sending_and_receiving
+ data["open_chat"] = viewing_messages_of
+
+ // silicons handle selecting photos a bit differently for now
+ if(!issilicon(user))
+ var/list/stored_photos = list()
+ for(var/datum/computer_file/picture/photo_file in computer.stored_files)
+ stored_photos += list(list(
+ "uid" = photo_file.uid,
+ "path" = SSassets.transport.get_asset_url(photo_file.picture_name)
+ ))
+ data["stored_photos"] = stored_photos
+ data["selected_photo_path"] = !isnull(selected_image) ? SSassets.transport.get_asset_url(selected_image) : null
+ data["on_spam_cooldown"] = !can_send_everyone_message()
+
+ var/obj/item/computer_disk/virus/disk = computer.inserted_disk
+ if(istype(disk))
+ data["virus_attach"] = TRUE
+ data["sending_virus"] = sending_virus
+ return data
+
+//////////////////////
+// MESSAGE HANDLING //
+//////////////////////
+
+/// Brings up the quick reply prompt to send a message.
+/datum/computer_file/program/messenger/proc/quick_reply_prompt(mob/living/user, datum/pda_chat/chat)
+ if(!istype(chat))
+ return
+ var/datum/computer_file/program/messenger/target = chat.recipient?.resolve()
+ if(!istype(target) || !istype(target.computer))
+ to_chat(user, span_notice("ERROR: Recipient no longer exists."))
+ chat.recipient = null
+ chat.can_reply = FALSE
+ return
+ var/target_name = target.computer.saved_identification
+ var/input_message = tgui_input_text(user, "Enter [mime_mode ? "emojis":"a message"]", "NT Messaging[target_name ? " ([target_name])" : ""]", encode = FALSE)
+ send_message(user, input_message, list(chat))
+
+/// Helper proc that sends a message to everyone
+/datum/computer_file/program/messenger/proc/send_message_to_all(mob/living/user, message)
+ var/list/datum/pda_chat/chats = list()
+ var/list/messenger_targets = list()
+
+ for(var/mc in get_messengers())
+ messenger_targets += mc
+
+ for(var/chatref in saved_chats)
+ var/datum/pda_chat/chat = saved_chats[chatref]
+ if(!(chat.recipient?.reference in messenger_targets)) // if its in messenger_targets, it's valid
+ continue
+ messenger_targets -= chat.recipient.reference
+ chats += chat
+
+ for(var/missing_messenger in messenger_targets)
+ var/datum/pda_chat/new_chat = create_chat(missing_messenger)
+ chats += new_chat
+
+ if(send_message(user, message, chats, everyone = TRUE))
+ COOLDOWN_START(src, last_text_everyone, 2 MINUTES)
+
+/// Creates a chat and adds it to saved_chats. Supports fake users. Returns the newly created chat.
+/datum/computer_file/program/messenger/proc/create_chat(recipient_ref, name, job)
+ var/datum/computer_file/program/messenger/recipient = null
+
+ if(isnull(name) && isnull(job))
+ if(!(recipient_ref in GLOB.pda_messengers))
+ CRASH("tried to create a chat with a messenger that isn't registered")
+ recipient = GLOB.pda_messengers[recipient_ref]
+
+ var/datum/pda_chat/new_chat = new(recipient)
+
+ // this is a chat with a "fake user" (automated or forged message)
+ if(!istype(recipient))
+ new_chat.cached_name = name
+ new_chat.cached_job = job
+ new_chat.can_reply = FALSE
+
+ saved_chats[REF(new_chat)] = new_chat
+
+ return new_chat
+
+/// Gets the chat by the recipient, either by their name or messenger ref
+/datum/computer_file/program/messenger/proc/find_chat_by_recipient(recipient, fake_user = FALSE)
+ for(var/chat_ref in saved_chats)
+ var/datum/pda_chat/chat = saved_chats[chat_ref]
+ if(fake_user && chat.cached_name == recipient)
+ return chat
+ else if(chat.recipient?.reference == recipient)
+ return chat
+ return null
+
+/// Returns a message input, sanitized and checked against the filter
+/datum/computer_file/program/messenger/proc/sanitize_pda_message(message, mob/sender)
+ message = sanitize(trim(message, MAX_PDA_MESSAGE_LEN))
+
+ if(mime_mode)
+ message = emoji_sanitize(message)
+
+ // check message against filter
+ if(!check_pda_message_against_filter(message, sender))
+ return null
+
+ return message
+
+/// Sends a message to targets via PDA. When sending to everyone, set `everyone` to true so the message is formatted accordingly
+/datum/computer_file/program/messenger/proc/send_message(mob/living/sender, message, list/targets, everyone = FALSE)
+ message = sanitize_pda_message(message, sender)
+
+ if(!message)
+ return FALSE
+
+ // upgrade the image asset to a permanent key
+ var/photo_asset_key = selected_image
+ if(photo_asset_key == TEMP_IMAGE_PATH(REF(src)))
+ var/datum/asset_cache_item/img_asset = SSassets.cache[photo_asset_key]
+ photo_asset_key = SSmodular_computers.get_next_picture_name()
+ SSassets.transport.register_asset(photo_asset_key, img_asset.resource, img_asset.hash)
+
+ // our sender targets
+ var/list/datum/computer_file/program/messenger/target_messengers = list()
+ var/list/datum/pda_chat/target_chats = list()
+
+ var/should_alert = length(targets) == 1
+
+ // filter out invalid targets
+ for(var/target in targets)
+ var/datum/pda_chat/target_chat = null
+ var/datum/computer_file/program/messenger/target_messenger = null
+
+ if(istype(target, /datum/pda_chat))
+ target_chat = target
+
+ if(!target_chat.can_reply)
+ if(should_alert)
+ to_chat(sender, span_notice("ERROR: Recipient has receiving disabled."))
+ continue
+
+ target_messenger = target_chat.recipient?.resolve()
+
+ if(!istype(target_messenger))
+ if(should_alert)
+ to_chat(sender, span_notice("ERROR: Recipient no longer exists."))
+ target_chat.can_reply = FALSE
+ target_chat.recipient = null
+ continue
+
+ if(!target_messenger.sending_and_receiving)
+ if(should_alert)
+ to_chat(sender, span_notice("ERROR: Recipient has receiving disabled."))
+ continue
+
+ else if(istype(target, /datum/computer_file/program/messenger))
+ target_messenger = target
+
+ if(!target_messenger.sending_and_receiving)
+ if(should_alert)
+ to_chat(sender, span_notice("ERROR: Recipient has receiving disabled."))
+ continue
+
+ target_chat = find_chat_by_recipient(REF(target))
+
+ if(!istype(target_chat))
+ target_chat = create_chat(REF(target))
+
+ else
+ stack_trace("invalid target [target]")
+ continue
+
+ target_chats += target_chat
+ target_messengers += target_messenger
+
+ if(!send_message_signal(sender, message, target_messengers, photo_asset_key, everyone))
+ return FALSE
+
+ // Log it in our logs
+ var/datum/pda_message/message_datum = new(message, TRUE, station_time_timestamp(PDA_MESSAGE_TIMESTAMP_FORMAT), photo_asset_key, everyone)
+ for(var/datum/pda_chat/target_chat as anything in target_chats)
+ target_chat.add_message(message_datum, show_in_recents = !everyone)
+ target_chat.unread_messages = 0
+
+ // send new pictures to everyone
+ if(!isnull(photo_asset_key))
+ update_pictures_for_all()
+
+ // switch our chat screen after sending a message, but do it only if it's not to everyone
+ if(!everyone)
+ viewing_messages_of = REF(target_chats[1])
+
+ return TRUE
+
+/// Sends a rigged message that explodes when the recipient tries to reply or look at it.
+/datum/computer_file/program/messenger/proc/send_rigged_message(mob/sender, message, list/datum/computer_file/program/messenger/targets, fake_name, fake_job, attach_fake_photo)
+ message = sanitize_pda_message(message, sender)
+
+ if(!message)
+ return FALSE
+
+ var/fake_photo = attach_fake_photo ? ">:3c" : null
+
+ return send_message_signal(sender, message, targets, fake_photo, FALSE, TRUE, fake_name, fake_job)
+
+/datum/computer_file/program/messenger/proc/send_message_signal(mob/sender, message, list/datum/computer_file/program/messenger/targets, photo_path = null, everyone = FALSE, rigged = FALSE, fake_name = null, fake_job = null)
+ if(!sender.can_perform_action(computer))
+ return FALSE
+
+ if(!COOLDOWN_FINISHED(src, last_text))
+ return FALSE
+
+ if(!length(targets))
+ return FALSE
+
+ // check for jammers
+ if(is_within_radio_jammer_range(computer) && !rigged)
+ // different message so people know it's a radio jammer
+ to_chat(sender, span_notice("ERROR: Network unavailable, please try again later."))
+ if(alert_able && !alert_silenced)
+ playsound(computer, 'sound/machines/terminal_error.ogg', 15, TRUE)
+ return FALSE
+
+ // used for logging
+ var/list/stringified_targets = list()
+
+ for(var/datum/computer_file/program/messenger/messenger as anything in targets)
+ stringified_targets += get_messenger_name(messenger)
+
+ var/datum/signal/subspace/messaging/tablet_message/signal = new(computer, list(
+ "ref" = REF(src),
+ "message" = message,
+ "targets" = targets,
+ "rigged" = rigged,
+ "everyone" = everyone,
+ "photo" = photo_path,
+ "automated" = FALSE,
+ ))
+ if(rigged) //Will skip the message server and go straight to the hub so it can't be cheesed by disabling the message server machine
+ signal.data["fakename"] = fake_name
+ signal.data["fakejob"] = fake_job
+ signal.server_type = /obj/machinery/telecomms/hub
+ signal.data["reject"] = FALSE // Do not refuse the message
+
+ signal.send_to_receivers()
+
+ // If it didn't reach, note that fact
+ if (!signal.data["done"])
+ to_chat(sender, span_notice("ERROR: Server is not responding."))
+ if(alert_able && !alert_silenced)
+ playsound(computer, 'sound/machines/terminal_error.ogg', 15, TRUE)
+ return FALSE
+
+ // Log in the talk log
+ sender.log_talk(message, LOG_PDA, tag="[rigged ? "Rigged" : ""] PDA: [computer.saved_identification] to [signal.format_target()]")
+ if(rigged)
+ log_bomber(sender, "sent a rigged PDA message (Name: [fake_name]. Job: [fake_job]) to [english_list(stringified_targets)] [!is_special_character(sender) ? "(SENT BY NON-ANTAG)" : ""]")
+
+ message = emoji_parse(message) //already sent- this just shows the sent emoji as one to the sender in the to_chat
+
+ // Show it to ghosts
+ var/ghost_message = span_name("[sender] [rigged ? "(as [fake_name]) Rigged " : ""]PDA Message --> [span_name("[signal.format_target()]")]: \"[signal.format_message()]\"")
+ for(var/mob/player_mob as anything in GLOB.current_observers_list)
+ if(player_mob.client && !player_mob.client?.prefs)
+ stack_trace("[player_mob] ([player_mob.ckey]) had null prefs, which shouldn't be possible!")
+ continue
+
+ if(isobserver(player_mob) && (player_mob.client?.prefs.chat_toggles & CHAT_GHOSTPDA))
+ to_chat(player_mob, "[FOLLOW_LINK(player_mob, sender)] [ghost_message]")
+
+ to_chat(sender, span_info("PDA message sent to [signal.format_target()]: \"[message]\""))
+
+ if (alert_able && !alert_silenced)
+ computer.send_sound()
+
+ COOLDOWN_START(src, last_text, 1 SECONDS)
+
+ selected_image = null
+ return TRUE
+
+/datum/computer_file/program/messenger/proc/receive_message(datum/signal/subspace/messaging/tablet_message/signal)
+ var/datum/pda_chat/chat = null
+
+ var/is_rigged = signal.data["rigged"]
+ var/is_automated = signal.data["automated"]
+ var/is_fake_user = is_rigged || is_automated || isnull(signal.data["ref"])
+ var/fake_name = is_fake_user ? signal.data["fakename"] : null
+ var/fake_job = is_fake_user ? signal.data["fakejob"] : null
+
+ var/sender_ref = signal.data["ref"]
+
+ // don't create a new chat for rigged messages, make it a one off notif
+ if(!is_rigged)
+ var/datum/pda_message/message = new(signal.data["message"], FALSE, station_time_timestamp(PDA_MESSAGE_TIMESTAMP_FORMAT), signal.data["photo"], signal.data["everyone"])
+
+ chat = find_chat_by_recipient(is_fake_user ? fake_name : sender_ref, is_fake_user)
+ if(!istype(chat))
+ chat = create_chat(!is_fake_user ? sender_ref : null, fake_name, fake_job)
+ chat.add_message(message)
+ chat.unread_messages++
+
+ // the recipient (us) currently has a chat with the sender open, so update their ui
+ if(!isnull(viewing_messages_of) && viewing_messages_of == sender_ref)
+ viewing_messages_of = REF(chat)
+
+ var/mob/living/receiver_mob = null
+ //Check our immediate loc
+ if(isliving(computer.loc))
+ receiver_mob = computer.loc
+ //Maybe they are a silicon!
+ else
+ receiver_mob = get(computer, /mob/living/silicon)
+
+ var/should_ring = !alert_silenced || is_rigged
+
+ if(should_ring && istype(receiver_mob) && receiver_mob.is_literate() && (receiver_mob.stat == CONSCIOUS || receiver_mob.stat == SOFT_CRIT))
+ var/reply_href = signal.data["rigged"] ? "explode" : "message"
+ var/photo_href = signal.data["rigged"] ? "explode" : "open"
+ var/reply
+ if(is_automated)
+ reply = "\[Automated Message\]"
+ else
+ reply = "(Reply)"
+
+ // resolving w/o nullcheck here, assume the messenger exists if a real person sent a message
+ var/datum/computer_file/program/messenger/sender_messenger = chat.recipient?.resolve()
+
+ var/sender_title = is_fake_user ? STRINGIFY_PDA_TARGET(fake_name, fake_job) : get_messenger_name(sender_messenger)
+ var/sender_name = is_fake_user ? fake_name : sender_messenger.computer.saved_identification
+
+ if (isAI(receiver_mob))
+ sender_title = "[sender_title]"
+
+ var/inbound_message = "[signal.format_message()]"
+ inbound_message = emoji_parse(inbound_message)
+
+ var/photo_message = signal.data["photo"] ? " (Photo Attached)" : ""
+ to_chat(receiver_mob, span_infoplain("[icon2html(computer, receiver_mob)] PDA message from [sender_title], \"[inbound_message]\"[photo_message] [reply]"))
+
+ if (alert_able && should_ring)
+ computer.ring(ringtone)
+
+ SStgui.update_uis(computer)
+ update_pictures_for_all()
+
+/// topic call that answers to people pressing "(Reply)" in chat
+/datum/computer_file/program/messenger/Topic(href, href_list)
+ ..()
+
+ if(QDELETED(src))
+ return
+ if(!usr.can_perform_action(computer, FORBID_TELEKINESIS_REACH))
+ return
+
+ // send an activation message and open the messenger
+ if(!(computer.enabled || computer.turn_on(usr, open_ui = FALSE)))
+ return
+ if(!(computer.active_program == src || computer.open_program(usr, src, open_ui = FALSE)))
+ return
+
+ var/target_href = href_list["target"]
+
+ switch(href_list["choice"])
+ if("message")
+ if(!(target_href in saved_chats))
+ return
+ quick_reply_prompt(usr, saved_chats[target_href])
+
+ if("open")
+ if(target_href in saved_chats)
+ viewing_messages_of = target_href
+ computer.update_tablet_open_uis(usr)
+
+ if("explode")
+ if(!HAS_TRAIT(computer, TRAIT_PDA_CAN_EXPLODE))
+ return
+
+ var/obj/item/modular_computer/pda/comp = computer
+ comp.explode(usr, from_message_menu = TRUE)
+
+#undef PDA_MESSAGE_TIMESTAMP_FORMAT
+#undef MAX_PDA_MESSAGE_LEN
+#undef TEMP_IMAGE_PATH
diff --git a/code/modules/modular_computers/file_system/programs/ntmessenger.dm b/code/modules/modular_computers/file_system/programs/ntmessenger.dm
deleted file mode 100644
index e61b7643b28..00000000000
--- a/code/modules/modular_computers/file_system/programs/ntmessenger.dm
+++ /dev/null
@@ -1,426 +0,0 @@
-/datum/computer_file/program/messenger
- filename = "nt_messenger"
- filedesc = "Direct Messenger"
- category = PROGRAM_CATEGORY_MISC
- program_icon_state = "command"
- extended_desc = "This program allows old-school communication with other modular devices."
- size = 0
- undeletable = TRUE // It comes by default in tablets, can't be downloaded, takes no space and should obviously not be able to be deleted.
- header_program = TRUE
- available_on_ntnet = FALSE
- usage_flags = PROGRAM_TABLET
- ui_header = "ntnrc_idle.gif"
- tgui_id = "NtosMessenger"
- program_icon = "comment-alt"
- alert_able = TRUE
-
- /// The current ringtone (displayed in the chat when a message is received).
- var/ringtone = MESSENGER_RINGTONE_DEFAULT
- /// Whether or not the ringtone is currently on.
- var/ringer_status = TRUE
- /// Whether or not we're sending and receiving messages.
- var/sending_and_receiving = TRUE
- /// The messages currently saved in the app.
- var/messages = list()
- /// great wisdom from PDA.dm - "no spamming" (prevents people from spamming the same message over and over)
- var/last_text
- /// even more wisdom from PDA.dm - "no everyone spamming" (prevents people from spamming the same message over and over)
- var/last_text_everyone
- /// Scanned photo for sending purposes.
- var/datum/picture/saved_image
- /// Whether the user is invisible to the message list.
- var/invisible = FALSE
- /// Whether or not we're currently looking at the message list.
- var/viewing_messages = FALSE
- // Whether or not this device is currently hidden from the message monitor.
- var/monitor_hidden = FALSE
- // Whether or not we're sorting by job.
- var/sort_by_job = TRUE
- // Whether or not we're sending (or trying to send) a virus.
- var/sending_virus = FALSE
-
- /// The path for the current loaded image in rsc
- var/photo_path
-
- /// Whether or not we're in a mime PDA.
- var/mime_mode = FALSE
- /// Whether this app can send messages to all.
- var/spam_mode = FALSE
-
-/datum/computer_file/program/messenger/application_attackby(obj/item/attacking_item, mob/living/user)
- if(!istype(attacking_item, /obj/item/photo))
- return FALSE
- var/obj/item/photo/pic = attacking_item
- saved_image = pic.picture
- ProcessPhoto()
- user.balloon_alert(user, "photo uploaded")
- return TRUE
-
-/datum/computer_file/program/messenger/proc/ScrubMessengerList()
- var/list/dictionary = list()
-
- for(var/obj/item/modular_computer/messenger in GetViewableDevices(sort_by_job))
- if(messenger.saved_identification && messenger.saved_job && !(messenger == computer))
- var/list/data = list()
- data["name"] = messenger.saved_identification
- data["job"] = messenger.saved_job
- data["ref"] = REF(messenger)
-
- //if(data["ref"] != REF(computer)) // you cannot message yourself (despite all my rage)
- dictionary += list(data)
-
- return dictionary
-
-/proc/GetViewableDevices(sort_by_job = FALSE)
- var/list/dictionary = list()
-
- var/sortmode
- if(sort_by_job)
- sortmode = GLOBAL_PROC_REF(cmp_pdajob_asc)
- else
- sortmode = GLOBAL_PROC_REF(cmp_pdaname_asc)
-
- for(var/obj/item/modular_computer/P in sort_list(GLOB.TabletMessengers, sortmode))
- for(var/datum/computer_file/program/messenger/app in P.stored_files)
- if(!P.saved_identification || !P.saved_job || app.invisible || app.monitor_hidden)
- continue
- dictionary += P
-
- return dictionary
-
-/datum/computer_file/program/messenger/proc/StringifyMessengerTarget(obj/item/modular_computer/messenger)
- return "[messenger.saved_identification] ([messenger.saved_job])"
-
-/datum/computer_file/program/messenger/proc/ProcessPhoto()
- if(saved_image)
- var/icon/img = saved_image.picture_image
- var/deter_path = "tmp_msg_photo[rand(0, 99999)].png"
- usr << browse_rsc(img, deter_path) // funny random assignment for now, i'll make an actual key later
- photo_path = deter_path
-
-/datum/computer_file/program/messenger/ui_state(mob/user)
- if(issilicon(user))
- return GLOB.reverse_contained_state
- return GLOB.default_state
-
-/datum/computer_file/program/messenger/ui_act(action, list/params, datum/tgui/ui)
- switch(action)
- if("PDA_ringSet")
- var/new_ringtone = tgui_input_text(usr, "Enter a new ringtone", "Ringtone", ringtone, MESSENGER_RINGTONE_MAX_LENGTH)
- var/mob/living/usr_mob = usr
- if(!new_ringtone || !in_range(computer, usr_mob) || computer.loc != usr_mob)
- return
-
- if(SEND_SIGNAL(computer, COMSIG_TABLET_CHANGE_ID, usr_mob, new_ringtone) & COMPONENT_STOP_RINGTONE_CHANGE)
- return
-
- ringtone = new_ringtone
- return TRUE
-
- if("PDA_ringer_status")
- ringer_status = !ringer_status
- return TRUE
-
- if("PDA_sAndR")
- sending_and_receiving = !sending_and_receiving
- return TRUE
-
- if("PDA_viewMessages")
- viewing_messages = !viewing_messages
- return TRUE
-
- if("PDA_clearMessages")
- messages = list()
- return TRUE
-
- if("PDA_changeSortStyle")
- sort_by_job = !sort_by_job
- return TRUE
-
- if("PDA_sendEveryone")
- if(!sending_and_receiving)
- to_chat(usr, span_notice("ERROR: Device has sending disabled."))
- return
-
- if(!spam_mode)
- to_chat(usr, span_notice("ERROR: Device does not have mass-messaging perms."))
- return
-
- var/list/targets = list()
-
- for(var/obj/item/modular_computer/mc in GetViewableDevices())
- targets += mc
-
- if(targets.len > 0)
- send_message(usr, targets, TRUE)
-
- return TRUE
-
- if("PDA_sendMessage")
- if(!sending_and_receiving)
- to_chat(usr, span_notice("ERROR: Device has sending disabled."))
- return
-
- var/obj/item/modular_computer/target = locate(params["ref"])
- if(!target)
- return // we don't want tommy sending his messages to nullspace
-
- if(!(target.saved_identification == params["name"] && target.saved_job == params["job"]))
- to_chat(usr, span_notice("ERROR: User no longer exists."))
- return
-
- for(var/datum/computer_file/program/messenger/app in computer.stored_files)
- if(!app.sending_and_receiving && !sending_virus)
- to_chat(usr, span_notice("ERROR: Device has receiving disabled."))
- return
-
- if(sending_virus)
- var/obj/item/computer_disk/virus/disk = computer.inserted_disk
- if(istype(disk))
- disk.send_virus(computer, target, usr)
- update_static_data(usr, ui)
- return TRUE
-
- send_message(usr, list(target))
- return TRUE
-
- if("PDA_clearPhoto")
- saved_image = null
- photo_path = null
- return TRUE
-
- if("PDA_toggleVirus")
- sending_virus = !sending_virus
- return TRUE
-
- if("PDA_selectPhoto")
- if(!issilicon(usr))
- return
- var/mob/living/silicon/user = usr
- if(!user.aicamera)
- return
- var/datum/picture/selected_photo = user.aicamera.selectpicture(user)
- if(!selected_photo)
- return
- saved_image = selected_photo
- ProcessPhoto()
- return TRUE
-
-/datum/computer_file/program/messenger/ui_static_data(mob/user)
- var/list/data = list()
- data["owner"] = computer.saved_identification
- return data
-
-/datum/computer_file/program/messenger/ui_data(mob/user)
- var/list/data = list()
- data["messages"] = messages
- data["sortByJob"] = sort_by_job
- data["isSilicon"] = issilicon(user)
- data["messengers"] = ScrubMessengerList()
- data["ringer_status"] = ringer_status
- data["sending_and_receiving"] = sending_and_receiving
- data["viewing_messages"] = viewing_messages
- data["photo"] = photo_path
- data["canSpam"] = spam_mode
-
- var/obj/item/computer_disk/virus/disk = computer.inserted_disk
- if(disk && istype(disk))
- data["virus_attach"] = TRUE
- data["sending_virus"] = sending_virus
- return data
-
-//////////////////////
-// MESSAGE HANDLING //
-//////////////////////
-
-///Gets an input message from user and returns the sanitized message.
-/datum/computer_file/program/messenger/proc/msg_input(mob/living/user, target_name, rigged = FALSE)
- var/input_message
- if(mime_mode)
- input_message = emoji_sanitize(tgui_input_text(user, "Enter emojis", "NT Messaging[target_name ? " ([target_name])" : ""]"))
- else
- input_message = tgui_input_text(user, "Enter a message", "NT Messaging[target_name ? " ([target_name])" : ""]")
-
- if (!input_message || !sending_and_receiving)
- return
- if(!user.can_perform_action(computer))
- return
- return sanitize(input_message)
-
-/datum/computer_file/program/messenger/proc/send_message(mob/living/user, list/obj/item/modular_computer/targets, everyone = FALSE, rigged = FALSE, fake_name = null, fake_job = null)
- if(!targets.len)
- return FALSE
-
- var/target_name = length(targets) == 1 ? targets[1].saved_identification : "Everyone"
- var/message = msg_input(user, target_name, rigged)
-
- if(!message)
- return FALSE
-
- if((last_text && world.time < last_text + 10) || (everyone && last_text_everyone && world.time < last_text_everyone + 2 MINUTES))
- to_chat(user, span_warning("The subspace transmitter of your tablet is still cooling down!"))
- return FALSE
-
- var/turf/position = get_turf(computer)
- for(var/obj/item/jammer/jammer as anything in GLOB.active_jammers)
- var/turf/jammer_turf = get_turf(jammer)
- if(position?.z == jammer_turf.z && (get_dist(position, jammer_turf) <= jammer.range))
- return FALSE
-
- var/list/filter_result = CAN_BYPASS_FILTER(user) ? null : is_ic_filtered_for_pdas(message)
- if (filter_result)
- REPORT_CHAT_FILTER_TO_USER(user, filter_result)
- return FALSE
-
- var/list/soft_filter_result = CAN_BYPASS_FILTER(user) ? null : is_soft_ic_filtered_for_pdas(message)
- if (soft_filter_result)
- if(tgui_alert(usr,"Your message contains \"[soft_filter_result[CHAT_FILTER_INDEX_WORD]]\". \"[soft_filter_result[CHAT_FILTER_INDEX_REASON]]\", Are you sure you want to send it?", "Soft Blocked Word", list("Yes", "No")) != "Yes")
- return FALSE
- message_admins("[ADMIN_LOOKUPFLW(usr)] has passed the soft filter for \"[soft_filter_result[CHAT_FILTER_INDEX_WORD]]\" they may be using a disallowed term in PDA messages. Message: \"[html_encode(message)]\"")
- log_admin_private("[key_name(usr)] has passed the soft filter for \"[soft_filter_result[CHAT_FILTER_INDEX_WORD]]\" they may be using a disallowed term in PDA messages. Message: \"[message]\"")
-
- // Send the signal
- var/list/string_targets = list()
- for (var/obj/item/modular_computer/comp in targets)
- if (comp.saved_identification && comp.saved_job) // != src is checked by the UI
- string_targets += STRINGIFY_PDA_TARGET(comp.saved_identification, comp.saved_job)
-
- if (!string_targets.len)
- return FALSE
- var/sent_prob = 1
- if(ishuman(user))
- var/mob/living/carbon/human/old_person = user
- sent_prob = old_person.age >= 30 ? 25 : sent_prob
- if (computer && prob(sent_prob))
- message += " [computer.get_messenger_ending()]"
-
- var/datum/signal/subspace/messaging/tablet_msg/signal = new(computer, list(
- "name" = fake_name || computer.saved_identification,
- "job" = fake_job || computer.saved_job,
- "message" = html_decode(message),
- "ref" = REF(computer),
- "targets" = targets,
- "rigged" = rigged,
- "photo" = saved_image,
- "photo_path" = photo_path,
- "automated" = FALSE,
- ))
- if(rigged) //Will skip the message server and go straight to the hub so it can't be cheesed by disabling the message server machine
- signal.data["rigged_user"] = REF(user) // Used for bomb logging
- signal.server_type = /obj/machinery/telecomms/hub
- signal.data["reject"] = FALSE // Do not refuse the message
-
- signal.send_to_receivers()
-
- // If it didn't reach, note that fact
- if (!signal.data["done"])
- to_chat(user, span_notice("ERROR: Server isn't responding."))
- if(ringer_status)
- playsound(src, 'sound/machines/terminal_error.ogg', 15, TRUE)
- return FALSE
-
- message = emoji_parse(message)//already sent- this just shows the sent emoji as one to the sender in the to_chat
- signal.data["message"] = emoji_parse(signal.data["message"])
-
- // Log it in our logs
- var/list/message_data = list()
- message_data["name"] = signal.data["name"]
- message_data["job"] = signal.data["job"]
- message_data["contents"] = html_decode(signal.format_message())
- message_data["outgoing"] = TRUE
- message_data["ref"] = signal.data["ref"]
- message_data["photo_path"] = signal.data["photo_path"]
- message_data["photo"] = signal.data["photo"]
- message_data["target_details"] = signal.format_target()
-
- // Show it to ghosts
- var/ghost_message = span_name("[message_data["name"]] [rigged ? "Rigged" : ""] PDA Message --> [span_name("[signal.format_target()]")]: