//Datum that's initialized on first calling the client procs. It's stored in /client //We keep a distinct names/refs list in an effort to make things speedy, and for easy checking if something is on our list. //We manually add to list element with procs to avoid dealing with clunky global lists that might not be relevant //and for ability to narrate from long range. /datum/entity_narrate var/list/entity_names = list() var/list/entity_refs = list() //TGUI Helper Vars var/tgui_id = "EntityNarrate" var/tgui_selection_mode = 0 //0 for single entity, 1 for multi entity var/tgui_selected_name = "" //String for single selection in-game name var/tgui_selected_type = "" //String for single selection type var/tgui_selected_id = "" //String to retrieve ref from entity_refs var/tgui_selected_refs //object references var/list/tgui_selected_id_multi = list() //List of strings containing mob ids for multi selection var/tgui_narrate_mode = 0 //0 for speak, 1 for emote var/tgui_narrate_privacy = 0 //0 for loud, 1 for subtle var/tgui_last_message = 0 // int to avoid spam //Appears as a right click verb on any obj and mob within view range. //when not right clicking we get a list to pick from in aforementioned view range. ADMIN_VERB_AND_CONTEXT_MENU(add_mob_for_narration, R_FUN, "Narrate Entity (Add ref)", "Saves a reference of target mob to be called when narrating.", "Fun.Narrate", E as obj|mob|turf in orange(world.view)) //Making sure we got the list datum on our client. if(!user.entity_narrate_holder) user.entity_narrate_holder = new /datum/entity_narrate() if(!istype(user.entity_narrate_holder, /datum/entity_narrate)) return var/datum/entity_narrate/holder = user.entity_narrate_holder //Since we extended to include all atoms, we're shutting things down with a guard clause for ghosts if(istype(E, /mob/observer)) to_chat(user, span_notice("Ghosts shouldn't be narrated! If you want a ghost, make it a subtype of mob/living!")) return //We require a static mob/living type to check for .client and also later on, to use the unique .say mechanics for stuttering and language if(isliving(E)) var/mob/living/L = E if(L.client) to_chat(user, span_notice("[L.name] is a player. All attempts to speak through them \ gets logged in case of abuse.")) log_and_message_admins("has added [L.ckey]'s mob to their entity narrate list", user) return var/unique_name = tgui_input_text(user, "Please give the entity a unique name to track internally. \ This doesn't override how it appears in game", "tracker", L.name, MAX_MESSAGE_LEN) if(unique_name in holder.entity_names) to_chat(user, span_notice("[unique_name] is not unique! Pick another!")) SSadmin_verbs.dynamic_invoke_verb(user, /datum/admin_verb/add_mob_for_narration, L) //Recursively calling ourselves until cancelled or a unique name is given. return holder.entity_names += unique_name holder.entity_refs[unique_name] = WEAKREF(L) log_and_message_admins("added [L.name] for their personal list to narrate", user) //Logging here to avoid spam, while still safeguarding abuse //Covering functionality for turfs and objs. We need static type to access the name var else if(istype(E, /atom)) var/atom/A = E var/unique_name = tgui_input_text(user, "Please give the entity a unique name to track internally. \ This doesn't override how it appears in game", "tracker", A.name, MAX_MESSAGE_LEN) if(unique_name in holder.entity_names) to_chat(user, span_notice("[unique_name] is not unique! Pick another!")) SSadmin_verbs.dynamic_invoke_verb(user, /datum/admin_verb/add_mob_for_narration, A) return holder.entity_names += unique_name holder.entity_refs[unique_name] = WEAKREF(A) log_and_message_admins("added [A.name] for their personal list to narrate", user) //Logging here to avoid spam, while still safeguarding abuse //Proc for keeping our ref list relevant, deleting mobs that are no longer relevant for our event ADMIN_VERB(remove_mob_for_narration, R_FUN, "Narrate Entity (Remove ref)", "Remove mobs you're no longer narrating from your list for easier work.", ADMIN_CATEGORY_FUN_NARRATE) if(!user.entity_narrate_holder) user.entity_narrate_holder = new /datum/entity_narrate() to_chat(user, "No references were added yet! First add references!") return if(!istype(user.entity_narrate_holder, /datum/entity_narrate)) return var/datum/entity_narrate/holder = user.entity_narrate_holder var/options = holder.entity_names + "Clear All" var/removekey = tgui_input_list(user, "Choose which entity to remove", "remove reference", options, null) if(removekey == "Clear All") if(tgui_alert(user, "Do you really want to clear your entity list?", "confirm", list("Yes", "No")) != "Yes") return holder.entity_names = list() holder.entity_refs = list() else if(removekey) holder.entity_refs -= removekey holder.entity_names -= removekey //Planned to have TGUI functionality //For now brings up a list of all entities on our reference list and gives us the option to choose what we wanna do //using TGUI/Byond list/alert inputs //Does not actually interact with the game world, it passes user input to narrate_mob_args(name, mode, message) after sanitizing ADMIN_VERB(narrate_mob, R_FUN, "Narrate Entity (Interface)", "Send either a visible or audiable message through your chosen entities using an interface.", ADMIN_CATEGORY_FUN_NARRATE) if(!user.entity_narrate_holder) user.entity_narrate_holder = new /datum/entity_narrate() to_chat(user, "No references were added yet! First add references!") return if(!istype(user.entity_narrate_holder, /datum/entity_narrate)) return var/datum/entity_narrate/holder = user.entity_narrate_holder //Obtaining and sanitizing arguments for the actual proc var/choices = holder.entity_names + "Open TGUI" var/which_entity = tgui_input_list(user, "Choose which mob to narrate", "Narrate mob", choices, null) if(!which_entity) return if(which_entity == "Open TGUI") holder.tgui_interact(user.mob) else var/mode = tgui_alert(user, "Speak or emote?", "mode", list("Speak", "Emote", "Cancel")) if(!mode || mode == "Cancel") return var/message = tgui_input_text(user, "Input what you want [which_entity] to [mode]", "narrate", null, multiline = TRUE, prevent_enter = TRUE) if(message) SSadmin_verbs.dynamic_invoke_verb(user, /datum/admin_verb/narrate_mob_args, which_entity, mode, message) //The actual logic of the verb. Called by narrate_mob() when used. ADMIN_VERB(narrate_mob_args, R_FUN, "Narrate Entity", "Narrate entities using positional arguments. Name should be as saved in ref list, mode should be Speak or Emote, follow with message.", "Fun.Narrate", name as text, mode as text, message as text) if(!user.entity_narrate_holder) user.entity_narrate_holder = new /datum/entity_narrate() to_chat(user, "No references were added yet! First add references!") return if(!istype(user.entity_narrate_holder, /datum/entity_narrate)) return var/datum/entity_narrate/holder = user.entity_narrate_holder //Sanitizing args name = sanitize(name) mode = sanitize(mode) if(!(mode in list("Speak", "Emote"))) to_chat(user, span_notice("Valid modes are 'Speak' and 'Emote'.")) return if(!holder.entity_refs[name]) to_chat(user, span_notice("[name] not in saved references!")) //Separate definition for mob/living and /obj due to .say() code allowing us to engage with languages, stuttering etc //We also need this so we can check for .client var/datum/weakref/wref = holder.entity_refs[name] var/selection = wref?.resolve() if(!selection) to_chat(user, span_notice("[name] has invalid reference, deleting")) holder.entity_names -= name holder.entity_refs -= name if(isliving(selection)) var/mob/living/our_entity = selection if(our_entity.client) //Making sure we can't speak for players log_and_message_admins("used entity-narrate to speak through [our_entity.ckey]'s mob", user) if(!message) message = tgui_input_text(user, "Input what you want [our_entity] to [mode]", "narrate", null, encode = FALSE) //say/emote sanitize already if(message && mode == "Speak") our_entity.say(message) else if(message && mode == "Emote") our_entity.custom_emote(VISIBLE_MESSAGE, message) else return //This does cost us some code duplication, but I think it's worth it. //furthermore, objs/turfs require the user to specify the verb when speaking, otherwise it looks like an emote. else if(istype(selection, /atom)) var/atom/our_entity = selection if(!message) message = tgui_input_text(user, "Input what you want [our_entity] to [mode]", "narrate", null) message = encode_html_emphasis(sanitize(message)) if(message && mode == "Speak") our_entity.audible_message(span_bold("[our_entity.name]") + " [message]") else if(message && mode == "Emote") our_entity.visible_message(span_bold("[our_entity.name]") + " [message]") else return /datum/entity_narrate/tgui_state(mob/user) return ADMIN_STATE(R_FUN) /datum/entity_narrate/tgui_interact(mob/user, datum/tgui/ui) ui = SStgui.try_update_ui(user, src, ui) if(!ui) ui = new(user, src, tgui_id, "Entity Narration") ui.open() /datum/entity_narrate/tgui_data(mob/user) var/list/data = list() data["mode_select"] = tgui_narrate_mode data["privacy_select"] = tgui_narrate_privacy data["selected_id"] = tgui_selected_id data["selected_name"] = tgui_selected_name data["selected_type"] = tgui_selected_type data["selection_mode"] = tgui_selection_mode data["multi_id_selection"] = tgui_selected_id_multi data["number_mob_selected"] = LAZYLEN(tgui_selected_id_multi) data["entity_names"] = entity_names return data /datum/entity_narrate/tgui_act(action, list/params, datum/tgui/ui) . = ..() if(.) return if(!check_rights_for(ui.user.client, R_FUN)) return switch(action) if("change_mode_multi") tgui_selection_mode = !tgui_selection_mode //Clearing selections after switching mode tgui_selected_id_multi = list() tgui_selected_id = "" tgui_selected_type = "" tgui_selected_name = "" tgui_selected_refs = null return TRUE if("change_mode_privacy") tgui_narrate_privacy = !tgui_narrate_privacy return TRUE if("change_mode_narration") tgui_narrate_mode = !tgui_narrate_mode return TRUE if("select_entity") if(tgui_selection_mode) if(params["id_selected"] in tgui_selected_id_multi) tgui_selected_id_multi -= params["id_selected"] else tgui_selected_id_multi += params["id_selected"] else if(params["id_selected"] in tgui_selected_id_multi) tgui_selected_id_multi -= params["id_selected"] tgui_selected_id = "" tgui_selected_type = "" tgui_selected_name = "" tgui_selected_refs = null else tgui_selected_id_multi = list() //Using the same var for ease of implementation. Thus, we must reset to empty each time. tgui_selected_id_multi += params["id_selected"] tgui_selected_id = params["id_selected"] var/datum/weakref/wref = entity_refs[tgui_selected_id] tgui_selected_refs = wref.resolve() if(!tgui_selected_refs) to_chat(ui.user, span_notice("[tgui_selected_id] has invalid reference, deleting")) entity_names -= tgui_selected_id entity_refs -= tgui_selected_id tgui_selected_id = "" tgui_selected_type = "" tgui_selected_name = "" tgui_selected_refs = null if(isliving(tgui_selected_refs)) var/mob/living/L = tgui_selected_refs if(L.client) tgui_selected_type = "!!!!PLAYER!!!!" tgui_selected_name = L.name else tgui_selected_type = L.type tgui_selected_name = L.name else if(istype(tgui_selected_refs, /atom)) var/atom/A = tgui_selected_refs tgui_selected_type = A.type tgui_selected_name = A.name return TRUE if("narrate") if(world.time < (tgui_last_message + 0.5 SECONDS)) to_chat(ui.user, span_notice("You can't messages that quickly! Wait at least half a second")) else to_chat(ui.user, span_notice("Message successfully sent!")) tgui_last_message = world.time var/message = params["message"] //Sanitizing before speaking it if(tgui_selection_mode) for(var/entity in tgui_selected_id_multi) var/datum/weakref/wref = entity_refs[entity] var/ref = wref.resolve() if(!ref) to_chat(ui.user, span_notice("[entity] has invalid reference, deleting")) entity_names -= entity entity_refs -= entity tgui_selected_id_multi -= entity continue if(isliving(ref)) var/mob/living/L = ref if(L.client) log_and_message_admins("used entity-narrate to speak through [L.ckey]'s mob", ui.user) narrate_tgui_mob(L, message) else if(istype(ref, /atom)) var/atom/A = ref narrate_tgui_atom(A, message) else var/datum/weakref/wref = entity_refs[tgui_selected_id] var/ref = wref.resolve() if(!ref) to_chat(ui.user, span_notice("[tgui_selected_id] has invalid reference, deleting")) entity_names -= tgui_selected_id entity_refs -= tgui_selected_id tgui_selected_id = "" tgui_selected_type = "" tgui_selected_name = "" tgui_selected_refs = null return TRUE if(isliving(ref)) var/mob/living/L = ref if(L.client) log_and_message_admins("used entity-narrate to speak through [L.ckey]'s mob", ui.user) narrate_tgui_mob(L, message) else if(istype(ref, /atom)) var/atom/A = ref narrate_tgui_atom(A, message) return TRUE /datum/entity_narrate/proc/narrate_tgui_mob(mob/living/L, message as text) //say and custom_emote sanitize it themselves, not sanitizing here to avoid double encoding. if(tgui_narrate_mode && tgui_narrate_privacy) L.custom_emote_vr(m_type = VISIBLE_MESSAGE, message = message) else if(tgui_narrate_mode && !tgui_narrate_privacy) L.custom_emote(VISIBLE_MESSAGE, message) else if(!tgui_narrate_mode && tgui_narrate_privacy) L.say(message, whispering = 1) else if(!tgui_narrate_mode && !tgui_narrate_privacy) L.say(message) /datum/entity_narrate/proc/narrate_tgui_atom(atom/A, message as text) message = encode_html_emphasis(sanitize(message)) if(tgui_narrate_mode && tgui_narrate_privacy) A.visible_message(span_italics(span_bold("\The [A.name]") + " [message]"), range = 1) else if(tgui_narrate_mode && !tgui_narrate_privacy) A.visible_message(span_bold("\The [A.name]") + " [message]",) else if(!tgui_narrate_mode && tgui_narrate_privacy) A.audible_message(span_italics(span_bold("\The [A.name]") + " [message]"), hearing_distance = 1) else if(!tgui_narrate_mode && !tgui_narrate_privacy) A.audible_message(span_bold("\The [A.name]") + " [message]")