SUBSYSTEM_DEF(typing) name = "Typing" flags = SS_BACKGROUND wait = 0.5 SECONDS /// The skin control to poll for TYPING_STATE_INPUT status. var/const/INPUT_HANDLE = "mainwindow.input" var/const/INFLIGHT_TIMEOUT = 5 SECONDS /// The status entry index of the related client's typing indicator visibility preference. var/const/INDEX_PREFERENCE = 1 /// The status entry index of the inflight state. var/const/INDEX_INFLIGHT = 2 /// The status entry index of the timeout threshold. var/const/INDEX_TIMEOUT = 3 /// The status entry index of the input bar typing state. var/const/INDEX_INPUT_STATE = 4 /// The status entry index of the verb input typing state. var/const/INDEX_VERB_STATE = 5 /// The highest index in a status entry. var/const/MAX_INDEX = 5 /// Matches input bar verbs that should set TYPING_STATE_INPUT. var/static/regex/match_verbs /// A list of clients waiting to be polled for input state. var/list/client/queue = list() /// A list of ckey to list, containing current state data. See .proc/get_entry for details. var/list/status = list() /* example of an entry: (ckey = list( preference = 0|1, inflight = 0|1, timeout = num, istyping_input = 0|1, istyping_hotkey = 0|1 ), ...) */ /datum/controller/subsystem/typing/Initialize(start_timeofday) match_verbs = regex("^(Me|Say|Whisper) +\"?\\w+") . = ..() /datum/controller/subsystem/typing/Recover() status = list() queue = list() /datum/controller/subsystem/typing/fire(resumed, no_mc_tick) if(!resumed) queue = global.clients.Copy() if(!length(queue)) return var/cut_until = 1 var/list/entry for (var/client/client as anything in queue) ++cut_until if(QDELETED(client)) continue entry = get_client_record(client) if(!islist(entry) || !entry[INDEX_PREFERENCE]) continue if(!entry[INDEX_INFLIGHT]) update_indicator_state_from_winget(client, entry) else if(world.time < entry[INDEX_TIMEOUT]) entry[INDEX_INFLIGHT] = FALSE if(no_mc_tick) CHECK_TICK else if(MC_TICK_CHECK) queue.Cut(1, cut_until) return queue.Cut() /// Return, generating if necessary, a ckey-indexed list holding typing status. /datum/controller/subsystem/typing/proc/get_client_record(client/client) PRIVATE_PROC(TRUE) var/ckey if(istext(client)) ckey = client else if(istype(client)) ckey = client.ckey else return var/list/entry = status[ckey] if(!entry) entry = new (MAX_INDEX) entry[INDEX_PREFERENCE] = !(client.prefs.toggles & SHOW_TYPING) entry[INDEX_INFLIGHT] = FALSE entry[INDEX_TIMEOUT] = world.time entry[INDEX_INPUT_STATE] = FALSE entry[INDEX_VERB_STATE] = FALSE status[ckey] = entry return entry /// Updates client's preference bool for whether typing indicators should be shown. /datum/controller/subsystem/typing/proc/update_preference(client/client, preference) var/list/entry = get_client_record(client) if(islist(entry)) entry[INDEX_PREFERENCE] = preference update_indicator(client, entry) /// Updates client|ckey's verb typing state to new_state. /datum/controller/subsystem/typing/proc/set_indicator_state(client/client, state) var/list/entry = get_client_record(client) if(islist(entry)) entry[INDEX_VERB_STATE] = state update_indicator(client, entry) /// Request client's input bar state using winget and updating entry accordingly. /datum/controller/subsystem/typing/proc/update_indicator_state_from_winget(client/client, list/entry) PRIVATE_PROC(TRUE) set waitfor = FALSE var/timeout = world.time + INFLIGHT_TIMEOUT entry[INDEX_INFLIGHT] = TRUE entry[INDEX_TIMEOUT] = timeout var/content = winget(client, INPUT_HANDLE, "text") if(timeout != entry[INDEX_TIMEOUT]) // We're stale. Touch nothing. return entry[INDEX_INFLIGHT] = FALSE if(!QDELETED(client)) entry[INDEX_INPUT_STATE] = match_verbs.Find(content) != 0 update_indicator(client, entry) /// Attempt to update the mob's typing state and indicator according to new state. /datum/controller/subsystem/typing/proc/update_indicator(client/client, list/entry) PRIVATE_PROC(TRUE) var/mob/target = client.mob var/display = (target.stat == CONSCIOUS || isobserver(target)) && entry[INDEX_PREFERENCE] && (entry[INDEX_INPUT_STATE] || entry[INDEX_VERB_STATE]) && isturf(target.loc) if(display == target.is_typing) return if(display) if(!target.typing_indicator) target.typing_indicator = new(null, target) target.is_typing = TRUE target.typing_indicator.show_typing_indicator() else if(target.typing_indicator) target.is_typing = FALSE target.typing_indicator.hide_typing_indicator() /* Typing indicators, when a mob uses the F3/F4 keys to bring the say/emote input boxes up this little buddy is made and follows them around until they are done (or something bad happens), helps tell nearby people that 'hey! I IS TYPIN'!' Updated 09/10/2022 to include chatbar using Spookerton's SStyping system from Polaris. */ /atom/movable/typing_indicator icon = 'icons/mob/talk.dmi' icon_state = "typing" mouse_opacity = MOUSE_OPACITY_TRANSPARENT vis_flags = VIS_INHERIT_ID var/atom/movable/master /atom/movable/typing_indicator/Initialize(ml, _master) . = ..() master = _master if(!ismovable(master)) stack_trace("Typing indicator initialized with [isnull(master) ? "null" : master] as master.") return INITIALIZE_HINT_QDEL if(ismob(master)) var/mob/mob = master mob.adjust_typing_indicator_offsets(src) /atom/movable/typing_indicator/Destroy() if(master) master.vis_contents -= src if(ismob(master)) var/mob/owner = master if(owner.typing_indicator == src) owner.typing_indicator = null return ..() /atom/movable/typing_indicator/proc/hide_typing_indicator() set waitfor = FALSE if(ismob(master)) var/mob/owner = master if(owner.is_typing) return animate(src, alpha = 0, time = 0.5 SECONDS, easing = EASE_IN) sleep(0.5 SECONDS) set_invisibility(INVISIBILITY_MAXIMUM) if(ismovable(master)) var/atom/movable/owner = master owner.vis_contents -= src /atom/movable/typing_indicator/proc/show_typing_indicator() // Make it visible after being hidden. set_invisibility(master.invisibility) // Update the appearance. if(ismob(master)) var/mob/owner = master var/speech_state_modifier = owner.get_speech_bubble_state_modifier() if(speech_state_modifier) icon_state = "[speech_state_modifier]_typing" else icon_state = initial(icon_state) if(ismovable(master)) var/atom/movable/owner = master owner.vis_contents += src // Animate it popping up from nowhere. var/matrix/M = matrix() M.Scale(0, 0) transform = M alpha = 0 animate(src, transform = 0, alpha = 255, time = 0.2 SECONDS, easing = EASE_IN)