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("PDA Photo" \ - + "" \ - + "" \ - + "", "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()]")]: [signal.format_message()]") - for(var/mob/player_mob in GLOB.player_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, user)] [ghost_message]") - - // Log in the talk log - user.log_talk(message, LOG_PDA, tag="[rigged ? "Rigged" : ""] PDA: [message_data["name"]] to [signal.format_target()]") - if(rigged) - log_bomber(user, "sent a rigged PDA message (Name: [message_data["name"]]. Job: [message_data["job"]]) to [english_list(string_targets)] [!is_special_character(user) ? "(SENT BY NON-ANTAG)" : ""]") - to_chat(user, span_info("PDA message sent to [signal.format_target()]: [signal.format_message()]")) - - if (ringer_status) - computer.send_sound() - - last_text = world.time - if (everyone) - message_data["name"] = "Everyone" - message_data["job"] = "" - last_text_everyone = world.time - - messages += list(message_data) - saved_image = null - photo_path = null - return TRUE - -/datum/computer_file/program/messenger/proc/receive_message(datum/signal/subspace/messaging/tablet_msg/signal) - 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"] = FALSE - message_data["ref"] = signal.data["ref"] - message_data["automated"] = signal.data["automated"] - message_data["photo_path"] = signal.data["photo_path"] - message_data["photo"] = signal.data["photo"] - messages += list(message_data) - - var/mob/living/L = null - if(computer.loc && isliving(computer.loc)) - L = computer.loc - //Maybe they are a pAI! - else - L = get(computer, /mob/living/silicon) - - if(L && (L.stat == CONSCIOUS || L.stat == SOFT_CRIT)) - var/reply = "(Reply)" - var/hrefstart - var/hrefend - if (isAI(L)) - hrefstart = "" - hrefend = "" - - if(signal.data["automated"]) - reply = "\[Automated Message\]" - - var/inbound_message = signal.format_message() - inbound_message = emoji_parse(inbound_message) - - if(L.is_literate()) - var/photo_message = message_data["photo"] ? " (Photo)" : "" - to_chat(L, span_infoplain("[icon2html(computer)] PDA message from [hrefstart][signal.data["name"]] ([signal.data["job"]])[hrefend], [inbound_message][photo_message] [reply]")) - - if (ringer_status) - computer.ring(ringtone) - -/// topic call that answers to people pressing "(Reply)" in chat -/datum/computer_file/program/messenger/Topic(href, href_list) - ..() - if(QDELETED(src)) - return - // send an activation message, open the messenger, kill whoever reads this nesting mess - if(!computer.enabled) - if(!computer.turn_on(usr, open_ui = FALSE)) - return - if(computer.active_program != src) - if(!computer.open_program(usr, src, open_ui = FALSE)) - return - if(!href_list["close"] && usr.can_perform_action(computer, FORBID_TELEKINESIS_REACH)) - switch(href_list["choice"]) - if("Message") - send_message(usr, list(locate(href_list["target"]))) - if("mess_us_up") - if(!HAS_TRAIT(src, TRAIT_PDA_CAN_EXPLODE)) - var/obj/item/modular_computer/pda/comp = computer - comp.explode(usr, from_message_menu = TRUE) - return diff --git a/code/modules/modular_computers/file_system/programs/wirecarp.dm b/code/modules/modular_computers/file_system/programs/wirecarp.dm index 9045fcc76b2..712d1e92cda 100644 --- a/code/modules/modular_computers/file_system/programs/wirecarp.dm +++ b/code/modules/modular_computers/file_system/programs/wirecarp.dm @@ -29,11 +29,10 @@ SSmodular_computers.purge_logs() return TRUE if("toggle_mass_pda") - var/obj/item/modular_computer/target_tablet = locate(params["ref"]) in GLOB.TabletMessengers - if(!istype(target_tablet)) + if(!(params["ref"] in GLOB.pda_messengers)) return - for(var/datum/computer_file/program/messenger/messenger_app in target_tablet.stored_files) - messenger_app.spam_mode = !messenger_app.spam_mode + var/datum/computer_file/program/messenger/target_messenger = GLOB.pda_messengers[params["ref"]] + target_messenger.spam_mode = !target_messenger.spam_mode return TRUE /datum/computer_file/program/ntnetmonitor/ui_data(mob/user) @@ -56,14 +55,14 @@ data["ntnetlogs"] += list(list("entry" = i)) data["tablets"] = list() - for(var/obj/item/modular_computer/messenger as anything in GetViewableDevices()) - var/list/tablet_data = list() - if(messenger.saved_identification) - for(var/datum/computer_file/program/messenger/messenger_app in computer.stored_files) - tablet_data["enabled_spam"] += messenger_app.spam_mode + for(var/messenger_ref in get_messengers_sorted_by_name()) + var/datum/computer_file/program/messenger/app = GLOB.pda_messengers[messenger_ref] + var/obj/item/modular_computer/pda = app.computer - tablet_data["name"] += messenger.saved_identification - tablet_data["ref"] += REF(messenger) + var/list/tablet_data = list() + tablet_data["enabled_spam"] = app.spam_mode + tablet_data["name"] = pda.saved_identification + tablet_data["ref"] = REF(app) data["tablets"] += list(tablet_data) diff --git a/tgstation.dme b/tgstation.dme index fe67d0a6b32..7ab9dac3e75 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -297,6 +297,7 @@ #include "code\__DEFINES\dcs\signals\signals_mind.dm" #include "code\__DEFINES\dcs\signals\signals_mining.dm" #include "code\__DEFINES\dcs\signals\signals_mod.dm" +#include "code\__DEFINES\dcs\signals\signals_modular_computer.dm" #include "code\__DEFINES\dcs\signals\signals_moveloop.dm" #include "code\__DEFINES\dcs\signals\signals_movetype.dm" #include "code\__DEFINES\dcs\signals\signals_music.dm" @@ -4495,6 +4496,7 @@ #include "code\modules\modular_computers\computers\machinery\modular_computer.dm" #include "code\modules\modular_computers\file_system\computer_file.dm" #include "code\modules\modular_computers\file_system\data.dm" +#include "code\modules\modular_computers\file_system\picture_file.dm" #include "code\modules\modular_computers\file_system\program.dm" #include "code\modules\modular_computers\file_system\programs\airestorer.dm" #include "code\modules\modular_computers\file_system\programs\alarm.dm" @@ -4514,7 +4516,6 @@ #include "code\modules\modular_computers\file_system\programs\notepad.dm" #include "code\modules\modular_computers\file_system\programs\nt_pay.dm" #include "code\modules\modular_computers\file_system\programs\ntdownloader.dm" -#include "code\modules\modular_computers\file_system\programs\ntmessenger.dm" #include "code\modules\modular_computers\file_system\programs\ntnrc_client.dm" #include "code\modules\modular_computers\file_system\programs\portrait_printer.dm" #include "code\modules\modular_computers\file_system\programs\powermonitor.dm" @@ -4537,6 +4538,8 @@ #include "code\modules\modular_computers\file_system\programs\maintenance\modsuit.dm" #include "code\modules\modular_computers\file_system\programs\maintenance\phys_scanner.dm" #include "code\modules\modular_computers\file_system\programs\maintenance\themes.dm" +#include "code\modules\modular_computers\file_system\programs\messenger\messenger_data.dm" +#include "code\modules\modular_computers\file_system\programs\messenger\messenger_program.dm" #include "code\modules\modular_computers\NTNet\NTNRC\conversation.dm" #include "code\modules\movespeed\_movespeed_modifier.dm" #include "code\modules\movespeed\modifiers\components.dm" diff --git a/tgui/packages/tgui/interfaces/AdminPDA.js b/tgui/packages/tgui/interfaces/AdminPDA.js index 9e620632607..c9a60dd6987 100644 --- a/tgui/packages/tgui/interfaces/AdminPDA.js +++ b/tgui/packages/tgui/interfaces/AdminPDA.js @@ -1,11 +1,11 @@ -import { useBackend, useLocalState } from '../backend'; import { Section, Dropdown, Input, Box, TextArea } from '../components'; -import { Button, ButtonCheckbox } from '../components/Button'; +import { useBackend, useLocalState } from '../backend'; +import { Button } from '../components/Button'; import { Window } from '../layouts'; export const AdminPDA = (props, context) => { return ( - + @@ -15,19 +15,32 @@ export const AdminPDA = (props, context) => { ); }; -export const ReceiverChoice = (props, context) => { - const { act, data } = useBackend(context); - const receivers = Array.from(data.users).sort(); +const ReceiverChoice = (props, context) => { + const { data } = useBackend(context); + const { users } = data; + const receivers = Array.from(Object.values(users)); const [user, setUser] = useLocalState(context, 'user', ''); const [spam, setSpam] = useLocalState(context, 'spam', false); + const [showInvisible, setShowInvisible] = useLocalState( + context, + 'showInvisible', + false + ); return (
showInvisible || !rcvr.invisible) + .map((rcvr) => ({ + displayText: rcvr.username, + value: rcvr.ref, + }))} width="275px" mb={1} onSelected={(value) => { @@ -36,15 +49,24 @@ export const ReceiverChoice = (props, context) => { /> - setSpam(!spam)}> - Should it be sent to everyone? - + setShowInvisible(!showInvisible)} + content="Include invisible?" + /> + setSpam(!spam)} + content="Should it be sent to everyone?" + />
); }; -export const SenderInfo = (props, context) => { +const SenderInfo = (props, context) => { const [name, setName] = useLocalState(context, 'name', ''); const [job, setJob] = useLocalState(context, 'job', ''); @@ -72,7 +94,7 @@ export const SenderInfo = (props, context) => { ); }; -export const MessageInput = (props, context) => { +const MessageInput = (props, context) => { const { act } = useBackend(context); const [user, setUser] = useLocalState(context, 'user', ''); @@ -80,9 +102,16 @@ export const MessageInput = (props, context) => { const [job, setJob] = useLocalState(context, 'job', ''); const [messageText, setMessageText] = useLocalState(context, 'message', ''); const [spam, setSpam] = useLocalState(context, 'spam', false); + const [force, setForce] = useLocalState(context, 'force', false); + const [showInvisible, setShowInvisible] = useLocalState( + context, + 'showInvisible', + false + ); - const tooltipText = function (name, job, message) { + const tooltipText = function (name, job, message, target) { let reasonList = []; + if (!target) reasonList.push('target'); if (!name) reasonList.push('name'); if (!job) reasonList.push('job'); if (!message) reasonList.push('message text'); @@ -104,11 +133,20 @@ export const MessageInput = (props, context) => { /> + setForce(!force)} + /> - - - )} - -
- - Detected Messengers - setSearchUser(value)} - mx={1} - ml={27} - /> -
-
- -
- - {users.length === 0 && 'No users found'} - {users.map((messenger) => ( - - ))} - - {!!canSpam && ( -
-
- {!owner && !isSilicon && } - - - ); -}; - -const MessageListScreen = (props, context) => { - const { act, data } = useBackend(context); - const { messages = [] } = data; - return ( - - - -
-
- {messages.map((message) => ( - -
- - {message.outgoing ? '(OUTGOING)' : '(INCOMING)'} - - {message.outgoing ? ( - {message.target_details} - ) : ( -
-
- {message.contents} - {!!message.photo && ( - - )} -
-
- ))} -
-
-
- ); -}; diff --git a/tgui/packages/tgui/interfaces/NtosMessenger/ChatScreen.tsx b/tgui/packages/tgui/interfaces/NtosMessenger/ChatScreen.tsx new file mode 100644 index 00000000000..e069126ee8b --- /dev/null +++ b/tgui/packages/tgui/interfaces/NtosMessenger/ChatScreen.tsx @@ -0,0 +1,430 @@ +import { Stack, Section, Button, Box, Input, Modal, Tooltip, Icon } from '../../components'; +import { Component, RefObject, createRef, SFC } from 'inferno'; +import { NtMessage, NtMessenger, NtPicture } from './types'; +import { BooleanLike } from 'common/react'; +import { useBackend } from '../../backend'; +import { decodeHtmlEntities } from 'common/string'; + +type ChatScreenProps = { + canReply: BooleanLike; + chatRef?: string; + isSilicon: BooleanLike; + messages: NtMessage[]; + recipient: NtMessenger; + selectedPhoto?: string; + sendingVirus: BooleanLike; + storedPhotos?: NtPicture[]; + unreads: number; +}; + +type ChatScreenState = { + canSend: boolean; + message: string; + previewingImage?: string; + selectingPhoto: boolean; +}; + +const READ_UNREADS_TIME_MS = 1000; +const SEND_COOLDOWN_MS = 1000; + +export class ChatScreen extends Component { + readUnreadsTimeout: NodeJS.Timeout | null = null; + scrollRef: RefObject; + + state: ChatScreenState = { + message: '', + selectingPhoto: false, + canSend: true, + }; + + constructor(props: ChatScreenProps) { + super(props); + + this.scrollRef = createRef(); + + this.scrollToBottom = this.scrollToBottom.bind(this); + this.handleMessageInput = this.handleMessageInput.bind(this); + this.handleSelectPicture = this.handleSelectPicture.bind(this); + this.handleSendMessage = this.handleSendMessage.bind(this); + this.trySetReadTimeout = this.trySetReadTimeout.bind(this); + this.tryClearReadTimeout = this.tryClearReadTimeout.bind(this); + this.clearUnreads = this.clearUnreads.bind(this); + } + + componentDidMount() { + this.scrollToBottom(); + this.trySetReadTimeout(); + } + + componentDidUpdate( + prevProps: ChatScreenProps, + _prevState: ChatScreenState, + _snapshot: any + ) { + if (prevProps.messages.length !== this.props.messages.length) { + this.scrollToBottom(); + this.trySetReadTimeout(); + } + } + + componentWillUnmount() { + if (!this.props.chatRef) { + return; + } + + const { act } = useBackend(this.context); + + this.tryClearReadTimeout(); + + act('PDA_saveMessageDraft', { + ref: this.props.chatRef, + message: this.state.message, + }); + } + + trySetReadTimeout() { + if (!this.props.chatRef) { + return; + } + const unreadMessages = this.props.unreads; + + this.tryClearReadTimeout(); + + if (unreadMessages > 0) { + this.readUnreadsTimeout = setTimeout(() => { + this.clearUnreads(); + this.readUnreadsTimeout = null; + }, READ_UNREADS_TIME_MS); + } + } + + tryClearReadTimeout() { + if (this.readUnreadsTimeout) { + clearTimeout(this.readUnreadsTimeout); + this.readUnreadsTimeout = null; + } + } + + clearUnreads() { + const { act } = useBackend(this.context); + + act('PDA_clearUnreads', { ref: this.props.chatRef }); + } + + scrollToBottom() { + const scroll = this.scrollRef.current; + if (scroll !== null) { + scroll.scrollTop = scroll.scrollHeight; + } + } + + handleSelectPicture() { + const { isSilicon } = this.props; + const { act } = useBackend(this.context); + if (isSilicon) { + act('PDA_siliconSelectPhoto'); + } else { + this.setState({ selectingPhoto: true }); + } + } + + handleSendMessage() { + if (this.state.message === '') { + return; + } + + const { act } = useBackend(this.context); + const { chatRef, recipient } = this.props; + + let ref = chatRef ? chatRef : recipient.ref; + + act('PDA_sendMessage', { + ref: ref, + message: this.state.message, + }); + + this.setState({ message: '', canSend: false }); + setTimeout(() => this.setState({ canSend: true }), SEND_COOLDOWN_MS); + } + + handleMessageInput(_: any, val: string) { + this.setState({ message: val }); + } + + render() { + const { act } = useBackend(this.context); + const { + canReply, + messages, + recipient, + chatRef, + selectedPhoto, + storedPhotos, + sendingVirus, + unreads, + } = this.props; + const { message, canSend, previewingImage, selectingPhoto } = this.state; + + let filteredMessages: JSX.Element[] = []; + + for (let index = 0; index < messages.length; index++) { + const message = messages[index]; + const isSwitch = !( + index === 0 || messages[index - 1].outgoing === message.outgoing + ); + + // this code shouldn't be reached if there's no chat + if (index === messages.length - unreads) { + filteredMessages.push(); + } + + filteredMessages.push( + + this.setState({ previewingImage: message.photo_path! }) + : undefined + } + /> + + ); + } + + let sendingBar: JSX.Element; + + if (!canReply) { + sendingBar = ( +
+ + You cannot reply to this user. + +
+ ); + } else if (selectingPhoto) { + const photos = storedPhotos!.map((photo) => ( + + + + )); + + sendingBar = ( +
+
+ ); + } else { + const attachmentButton = sendingVirus ? ( + + + )} + + + + + + {buttons} + + + + + ); + } + + return ( + +
+
+ + +
+ + {!!(messages.length > 0 && canReply) && ( + <> + + This is the beginning of your chat with {recipient.name}. + + + + )} + + {filteredMessages} + +
+
+ + {sendingBar} + {previewingImage && ( + +
this.setState({ previewingImage: undefined })} + /> + }> + +
+
+ )} +
+ ); + } +} + +type ChatMessageProps = { + outgoing: BooleanLike; + message: string; + everyone: BooleanLike; + timestamp: string; + photoPath?: string; + onPreviewImage?: () => void; +}; + +const ChatMessage = (props: ChatMessageProps) => { + const { message, everyone, outgoing, photoPath, timestamp, onPreviewImage } = + props; + + const displayMessage = decodeHtmlEntities(message); + + return ( + + + {displayMessage} + + + + + {!!everyone && ( + Sent to everyone + )} + {photoPath !== null && ( + + )} + + ); +}; + +const ChatDivider: SFC<{ mt: number }> = (props) => { + return ( + +
+ Unread Messages +
+ + ); +}; diff --git a/tgui/packages/tgui/interfaces/NtosMessenger/index.tsx b/tgui/packages/tgui/interfaces/NtosMessenger/index.tsx new file mode 100644 index 00000000000..1b227ab09df --- /dev/null +++ b/tgui/packages/tgui/interfaces/NtosMessenger/index.tsx @@ -0,0 +1,336 @@ +import { Box, Button, Icon, Section, Stack, Input, TextArea, Dimmer, Divider } from '../../components'; +import { useBackend, useLocalState } from '../../backend'; +import { createSearch } from 'common/string'; +import { BooleanLike } from 'common/react'; +import { NtosWindow } from '../../layouts'; + +import { NtChat, NtMessenger, NtPicture } from './types'; +import { ChatScreen } from './ChatScreen'; +import { sortBy } from 'common/collections'; + +type NtosMessengerData = { + can_spam: BooleanLike; + is_silicon: BooleanLike; + owner?: NtMessenger; + saved_chats: Record; + messengers: Record; + sort_by_job: BooleanLike; + alert_silenced: BooleanLike; + alert_able: BooleanLike; + sending_and_receiving: BooleanLike; + open_chat: string; + stored_photos?: NtPicture[]; + selected_photo_path?: string; + on_spam_cooldown: BooleanLike; + virus_attach: BooleanLike; + sending_virus: BooleanLike; +}; + +export const NtosMessenger = (props, context) => { + const { data } = useBackend(context); + const { + is_silicon, + saved_chats, + stored_photos, + selected_photo_path, + open_chat, + messengers, + sending_virus, + } = data; + + let content: JSX.Element; + if (open_chat !== null) { + const openChat = saved_chats[open_chat]; + const temporaryRecipient = messengers[open_chat]; + + content = ( + + ); + } else { + content = ; + } + + return ( + + {content} + + ); +}; + +const ContactsScreen = (props: any, context: any) => { + const { act, data } = useBackend(context); + const { + owner, + alert_silenced, + alert_able, + sending_and_receiving, + saved_chats, + messengers, + sort_by_job, + can_spam, + is_silicon, + virus_attach, + sending_virus, + } = data; + + const [searchUser, setSearchUser] = useLocalState(context, 'searchUser', ''); + + const sortByUnreads = sortBy((chat) => chat.unread_messages); + + const searchChatByName = createSearch( + searchUser, + (chat: NtChat) => chat.recipient.name + chat.recipient.job + ); + const searchMessengerByName = createSearch( + searchUser, + (messenger: NtMessenger) => messenger.name + messenger.job + ); + + const chatToButton = (chat: NtChat) => { + return ( + + ); + }; + + const messengerToButton = (messenger: NtMessenger) => { + return ( + + ); + }; + + const openChatsArray = sortByUnreads(Object.values(saved_chats)).filter( + searchChatByName + ); + + const filteredChatButtons = openChatsArray + .filter((c) => c.visible) + .map(chatToButton); + + const messengerButtons = Object.entries(messengers) + .filter( + ([ref, messenger]) => + openChatsArray.every((chat) => chat.recipient.ref !== ref) && + searchMessengerByName(messenger) + ) + .map(([_, messenger]) => messenger) + .map(messengerToButton) + .concat(openChatsArray.filter((chat) => !chat.visible).map(chatToButton)); + + const noId = !owner && !is_silicon; + + return ( + + +
+ + + + SpaceMessenger V6.5.3 + + + Bringing you spy-proof communications since 2467. + +
+
+ {filteredChatButtons.length > 0 && ( + + +
+ + Previous Messages +
+
+ {filteredChatButtons} +
+
+
+ )} + + +
+ + + + Detected Messengers + + +
+
+ + {messengerButtons.length === 0 && ( + + + + No users found. + + + )} + {messengerButtons} + +
+
+
+ {!!can_spam && ( + + + + )} + {noId && } +
+ ); +}; + +type ChatButtonProps = { + name: string; + unreads: number; + chatRef: string; +}; + +const ChatButton = (props: ChatButtonProps, context) => { + const { act } = useBackend(context); + const unreadMessages = props.unreads; + const hasUnreads = unreadMessages > 0; + return ( + + ); +}; + +const SendToAllSection = (props, context) => { + const { data, act } = useBackend(context); + const { on_spam_cooldown } = data; + + const [message, setmessage] = useLocalState(context, 'everyoneMessage', ''); + + return ( + <> +
+ + + + Send To All + + + + + +
+
+