mirror of
https://github.com/Bubberstation/Bubberstation.git
synced 2025-12-16 04:34:21 +00:00
## About The Pull Request `SSpolling.poll_ghosts_for_target` returns a 0 length list when no candidates are chosen so it will spawn an AI core without a client, so instead of `if(isnull(ghost)` we do `if(!ismob(ghost))`. closes https://github.com/tgstation/tgstation/issues/86976 ## Changelog 🆑 grungussuss fix: fixed a clientless AI spawning when a ghost poll for syndicate modsuit AI had no volunteers admin: AI rolebanned players can no longer role for Syndicate modsuit AI /🆑
343 lines
14 KiB
Plaintext
343 lines
14 KiB
Plaintext
SUBSYSTEM_DEF(polling)
|
|
name = "Polling"
|
|
flags = SS_BACKGROUND | SS_NO_INIT
|
|
wait = 1 SECONDS
|
|
runlevels = RUNLEVEL_GAME
|
|
/// List of polls currently ongoing, to be checked on next fire()
|
|
var/list/datum/candidate_poll/currently_polling
|
|
/// Number of polls performed since the start
|
|
var/total_polls = 0
|
|
|
|
/datum/controller/subsystem/polling/fire()
|
|
if(!currently_polling) // if polls_active is TRUE then this shouldn't happen, but still..
|
|
currently_polling = list()
|
|
|
|
for(var/datum/candidate_poll/running_poll as anything in currently_polling)
|
|
if(running_poll.time_left() <= 0)
|
|
polling_finished(running_poll)
|
|
|
|
/**
|
|
* Starts a poll.
|
|
*
|
|
* Arguments
|
|
* * question: Optional, The question to ask the candidates. If null, a default question will be used. ("Do you want to play as role?")
|
|
* * role: Optional, An antag role (IE, ROLE_TRAITOR) to pass, it won't show to any candidates who don't have it in their preferences.
|
|
* * check_jobban: Optional, What jobban role / flag to check, it won't show to any candidates who have this jobban.
|
|
* * poll_time: How long the poll will last.
|
|
* * ignore_category: Optional, A poll category. If a candidate has this category in their ignore list, they won't be polled.
|
|
* * flash_window: If TRUE, the candidate's window will flash when they're polled.
|
|
* * list/group: A list of candidates to poll.
|
|
* * alert_pic: Optional, An /atom or an /image to display on the poll alert.
|
|
* * jump_target: An /atom to teleport/jump to, if alert_pic is an /atom defaults to that.
|
|
* * role_name_text: Optional, A string to display in logging / the (default) question. If null, the role name will be used.
|
|
* * list/custom_response_messages: Optional, A list of strings to use as responses to the poll. If null, the default responses will be used. see __DEFINES/polls.dm for valid keys to use.
|
|
* * start_signed_up: If TRUE, all candidates will start signed up for the poll, making it opt-out rather than opt-in.
|
|
* * amount_to_pick: Lets you pick candidates and return a single mob or list of mobs that were chosen.
|
|
* * chat_text_border_icon: Object or path to make an icon of to decorate the chat announcement.
|
|
* * announce_chosen: Whether we should announce the chosen candidates in chat. This is ignored unless amount_to_pick is greater than 0.
|
|
*
|
|
* Returns a list of all mobs who signed up for the poll, OR, in the case that amount_to_pick is equal to 1 the singular mob/null if no available candidates.
|
|
*/
|
|
/datum/controller/subsystem/polling/proc/poll_candidates(
|
|
question,
|
|
role,
|
|
check_jobban,
|
|
poll_time = 30 SECONDS,
|
|
ignore_category = null,
|
|
flash_window = TRUE,
|
|
list/group = null,
|
|
alert_pic,
|
|
jump_target,
|
|
role_name_text,
|
|
list/custom_response_messages,
|
|
start_signed_up = FALSE,
|
|
amount_to_pick = 0,
|
|
chat_text_border_icon,
|
|
announce_chosen = TRUE,
|
|
)
|
|
if(group.len == 0)
|
|
return
|
|
if(role && !role_name_text)
|
|
role_name_text = role
|
|
if(role_name_text && !question)
|
|
question = "Do you want to play as [span_notice(role_name_text)]?"
|
|
if(!question)
|
|
question = "Do you want to play as a special role?"
|
|
log_game("Polling candidates [role_name_text ? "for [role_name_text]" : "\"[question]\""] for [DisplayTimeText(poll_time)] seconds")
|
|
|
|
// Start firing
|
|
total_polls++
|
|
|
|
if(isnull(jump_target) && isatom(alert_pic))
|
|
jump_target = alert_pic
|
|
|
|
var/datum/candidate_poll/new_poll = new(role_name_text, question, poll_time, ignore_category, jump_target, custom_response_messages)
|
|
LAZYADD(currently_polling, new_poll)
|
|
|
|
var/category = "[new_poll.poll_key]_poll_alert"
|
|
|
|
for(var/mob/candidate_mob as anything in group)
|
|
if(!candidate_mob.client)
|
|
continue
|
|
// Universal opt-out for all players.
|
|
if(!candidate_mob.client.prefs.read_preference(/datum/preference/toggle/ghost_roles))
|
|
continue
|
|
// Opt-out for admins whom are currently adminned.
|
|
if((!candidate_mob.client.prefs.read_preference(/datum/preference/toggle/ghost_roles_as_admin)) && candidate_mob.client.holder)
|
|
continue
|
|
if(!is_eligible(candidate_mob, role, check_jobban, ignore_category))
|
|
continue
|
|
|
|
if(start_signed_up)
|
|
new_poll.sign_up(candidate_mob, TRUE)
|
|
if(flash_window)
|
|
window_flash(candidate_mob.client)
|
|
|
|
// If we somehow send two polls for the same mob type, but with a duration on the second one shorter than the time left on the first one,
|
|
// we need to keep the first one's timeout rather than use the shorter one
|
|
var/atom/movable/screen/alert/poll_alert/current_alert = LAZYACCESS(candidate_mob.alerts, category)
|
|
var/alert_time = poll_time
|
|
var/datum/candidate_poll/alert_poll = new_poll
|
|
if(current_alert && current_alert.timeout > (world.time + poll_time - world.tick_lag))
|
|
alert_time = current_alert.timeout - world.time + world.tick_lag
|
|
alert_poll = current_alert.poll
|
|
|
|
// Send them an on-screen alert
|
|
var/atom/movable/screen/alert/poll_alert/poll_alert_button = candidate_mob.throw_alert(category, /atom/movable/screen/alert/poll_alert, timeout_override = alert_time, no_anim = TRUE)
|
|
if(!poll_alert_button)
|
|
continue
|
|
|
|
new_poll.alert_buttons += poll_alert_button
|
|
new_poll.RegisterSignal(poll_alert_button, COMSIG_QDELETING, TYPE_PROC_REF(/datum/candidate_poll, clear_alert_ref))
|
|
|
|
poll_alert_button.icon = ui_style2icon(candidate_mob.client?.prefs?.read_preference(/datum/preference/choiced/ui_style))
|
|
poll_alert_button.desc = "[question]"
|
|
poll_alert_button.show_time_left = TRUE
|
|
poll_alert_button.poll = alert_poll
|
|
poll_alert_button.set_role_overlay()
|
|
poll_alert_button.update_stacks_overlay()
|
|
poll_alert_button.update_candidates_number_overlay()
|
|
poll_alert_button.update_signed_up_overlay()
|
|
|
|
|
|
// Sign up inheritance and stacking
|
|
for(var/datum/candidate_poll/other_poll as anything in currently_polling)
|
|
if(new_poll == other_poll || new_poll.poll_key != other_poll.poll_key)
|
|
continue
|
|
// If there's already a poll for an identical mob type ongoing and the client is signed up for it, sign them up for this one
|
|
if((candidate_mob in other_poll.signed_up) && new_poll.sign_up(candidate_mob, TRUE))
|
|
break
|
|
|
|
// Image to display
|
|
var/image/poll_image
|
|
if(ispath(alert_pic, /atom) || isatom(alert_pic))
|
|
poll_image = new /mutable_appearance(alert_pic)
|
|
poll_image.pixel_z = 0
|
|
else if(!isnull(alert_pic))
|
|
poll_image = alert_pic
|
|
else
|
|
poll_image = image('icons/effects/effects.dmi', icon_state = "static")
|
|
|
|
if(poll_image)
|
|
poll_image.layer = FLOAT_LAYER
|
|
poll_image.plane = poll_alert_button.plane
|
|
poll_alert_button.add_overlay(poll_image)
|
|
|
|
// Chat message
|
|
var/act_jump = ""
|
|
var/custom_link_style_start = "<style>a:visited{color:Crimson !important}</style>"
|
|
var/custom_link_style_end = "style='color:DodgerBlue;font-weight:bold;-dm-text-outline: 1px black'"
|
|
if(isatom(alert_pic) && isobserver(candidate_mob))
|
|
act_jump = "[custom_link_style_start]<a href='?src=[REF(poll_alert_button)];jump=1'[custom_link_style_end]>\[Teleport\]</a>"
|
|
var/act_signup = "[custom_link_style_start]<a href='?src=[REF(poll_alert_button)];signup=1'[custom_link_style_end]>\[[start_signed_up ? "Opt out" : "Sign Up"]\]</a>"
|
|
var/act_never = ""
|
|
if(ignore_category)
|
|
act_never = "[custom_link_style_start]<a href='?src=[REF(poll_alert_button)];never=1'[custom_link_style_end]>\[Never For This Round\]</a>"
|
|
|
|
if(!duplicate_message_check(alert_poll)) //Only notify people once. They'll notice if there are multiple and we don't want to spam people.
|
|
SEND_SOUND(candidate_mob, 'sound/announcer/notice/notice2.ogg')
|
|
var/surrounding_icon
|
|
if(chat_text_border_icon)
|
|
var/image/surrounding_image
|
|
if(!ispath(chat_text_border_icon))
|
|
var/mutable_appearance/border_image = chat_text_border_icon
|
|
surrounding_image = border_image
|
|
else
|
|
surrounding_image = image(chat_text_border_icon)
|
|
surrounding_icon = icon2html(surrounding_image, candidate_mob, extra_classes = "bigicon")
|
|
var/final_message = examine_block("<span style='text-align:center;display:block'>[surrounding_icon] <span style='font-size:1.2em'>[span_ooc(question)]</span> [surrounding_icon]\n[act_jump] [act_signup] [act_never]</span>")
|
|
to_chat(candidate_mob, final_message)
|
|
|
|
// Start processing it so it updates visually the timer
|
|
START_PROCESSING(SSprocessing, poll_alert_button)
|
|
|
|
// Sleep until the time is up
|
|
UNTIL(new_poll.finished)
|
|
if(!amount_to_pick)
|
|
return new_poll.signed_up
|
|
if (!length(new_poll.signed_up))
|
|
return null
|
|
for(var/pick in 1 to amount_to_pick)
|
|
// There may be less people signed up than amount_to_pick
|
|
// pick_n_take returns the default return value of null if passed an empty list, so just break in that case rather than adding null to the list.
|
|
if(!length(new_poll.signed_up))
|
|
break
|
|
new_poll.chosen_candidates += pick_n_take(new_poll.signed_up)
|
|
if(announce_chosen)
|
|
new_poll.announce_chosen(group)
|
|
if(new_poll.chosen_candidates.len == 1)
|
|
var/chosen_one = pick(new_poll.chosen_candidates)
|
|
return chosen_one
|
|
return new_poll.chosen_candidates
|
|
|
|
/datum/controller/subsystem/polling/proc/poll_ghost_candidates(
|
|
question,
|
|
role,
|
|
check_jobban,
|
|
poll_time = 30 SECONDS,
|
|
ignore_category = null,
|
|
flashwindow = TRUE,
|
|
alert_pic,
|
|
jump_target,
|
|
role_name_text,
|
|
list/custom_response_messages,
|
|
start_signed_up = FALSE,
|
|
amount_to_pick = 0,
|
|
chat_text_border_icon,
|
|
announce_chosen = TRUE,
|
|
)
|
|
var/list/candidates = list()
|
|
if(!(GLOB.ghost_role_flags & GHOSTROLE_STATION_SENTIENCE))
|
|
return
|
|
for(var/mob/dead/observer/ghost_player in GLOB.player_list)
|
|
candidates += ghost_player
|
|
|
|
#ifdef TESTING
|
|
for(var/mob/dude in GLOB.player_list)
|
|
candidates |= dude
|
|
#endif
|
|
|
|
return poll_candidates(question, role, check_jobban, poll_time, ignore_category, flashwindow, candidates, alert_pic, jump_target, role_name_text, custom_response_messages, start_signed_up, amount_to_pick, chat_text_border_icon, announce_chosen)
|
|
|
|
/datum/controller/subsystem/polling/proc/poll_ghosts_for_target(
|
|
question,
|
|
role,
|
|
check_jobban,
|
|
poll_time = 30 SECONDS,
|
|
atom/movable/checked_target,
|
|
ignore_category = null,
|
|
flashwindow = TRUE,
|
|
alert_pic,
|
|
jump_target,
|
|
role_name_text,
|
|
list/custom_response_messages,
|
|
start_signed_up = FALSE,
|
|
chat_text_border_icon,
|
|
announce_chosen = TRUE,
|
|
)
|
|
var/static/list/atom/movable/currently_polling_targets = list()
|
|
if(currently_polling_targets.Find(checked_target))
|
|
return
|
|
currently_polling_targets += checked_target
|
|
var/mob/chosen_one = poll_ghost_candidates(question, role, check_jobban, poll_time, ignore_category, flashwindow, alert_pic, jump_target, role_name_text, custom_response_messages, start_signed_up, amount_to_pick = 1, chat_text_border_icon = chat_text_border_icon, announce_chosen = announce_chosen)
|
|
currently_polling_targets -= checked_target
|
|
if(!checked_target || QDELETED(checked_target) || !checked_target.loc)
|
|
return null
|
|
return chosen_one
|
|
|
|
/datum/controller/subsystem/polling/proc/poll_ghosts_for_targets(
|
|
question,
|
|
role,
|
|
check_jobban,
|
|
poll_time = 30 SECONDS,
|
|
list/checked_targets,
|
|
ignore_category = null,
|
|
flashwindow = TRUE,
|
|
alert_pic,
|
|
jump_target,
|
|
role_name_text,
|
|
list/custom_response_messages,
|
|
start_signed_up = FALSE,
|
|
chat_text_border_icon,
|
|
)
|
|
var/list/candidate_list = poll_ghost_candidates(question, role, check_jobban, poll_time, ignore_category, flashwindow, alert_pic, jump_target, role_name_text, custom_response_messages, start_signed_up, chat_text_border_icon = chat_text_border_icon)
|
|
for(var/atom/movable/potential_target as anything in checked_targets)
|
|
if(QDELETED(potential_target) || !potential_target.loc)
|
|
checked_targets -= potential_target
|
|
if(!length(checked_targets))
|
|
return list()
|
|
return candidate_list
|
|
|
|
/datum/controller/subsystem/polling/proc/is_eligible(mob/potential_candidate, role, check_jobban, the_ignore_category)
|
|
if(isnull(potential_candidate.key) || isnull(potential_candidate.client))
|
|
return FALSE
|
|
if(the_ignore_category)
|
|
if(potential_candidate.ckey in GLOB.poll_ignore[the_ignore_category])
|
|
return FALSE
|
|
if(role)
|
|
if(!(role in potential_candidate.client.prefs.be_special))
|
|
return FALSE
|
|
var/required_time = GLOB.special_roles[role] || 0
|
|
if(potential_candidate.client && potential_candidate.client.get_remaining_days(required_time) > 0)
|
|
return FALSE
|
|
|
|
if(check_jobban)
|
|
if(is_banned_from(potential_candidate.ckey, list(ROLE_SYNDICATE) + check_jobban))
|
|
return FALSE
|
|
|
|
//SKYRAT EDIT ADDITION BEGIN
|
|
if(is_banned_from(potential_candidate.ckey, BAN_GHOST_TAKEOVER) || is_banned_from(potential_candidate.ckey, BAN_ANTAGONIST))
|
|
to_chat(potential_candidate, "There was a ghost prompt for: [role], unfortunately you are banned from ghost takeovers.")
|
|
return FALSE
|
|
//SKYRAT EDIT END
|
|
|
|
return TRUE
|
|
|
|
/datum/controller/subsystem/polling/proc/polling_finished(datum/candidate_poll/finishing_poll)
|
|
currently_polling -= finishing_poll
|
|
// Trim players who aren't eligible anymore
|
|
var/length_pre_trim = length(finishing_poll.signed_up)
|
|
finishing_poll.trim_candidates()
|
|
log_game("Candidate poll [finishing_poll.role ? "for [finishing_poll.role]" : "\"[finishing_poll.question]\""] finished. [length_pre_trim] players signed up, [length(finishing_poll.signed_up)] after trimming")
|
|
finishing_poll.finished = TRUE
|
|
|
|
// Take care of updating the remaining screen alerts if a similar poll is found, or deleting them.
|
|
if(length(finishing_poll.alert_buttons))
|
|
for(var/atom/movable/screen/alert/poll_alert/alert as anything in finishing_poll.alert_buttons)
|
|
if(duplicate_message_check(finishing_poll))
|
|
alert.update_stacks_overlay()
|
|
else
|
|
alert.owner.clear_alert("[finishing_poll.poll_key]_poll_alert")
|
|
|
|
//More than enough time for the the `UNTIL()` stopping loop in `poll_candidates()` to be over, and the results to be turned in.
|
|
QDEL_IN(finishing_poll, 0.5 SECONDS)
|
|
|
|
/datum/controller/subsystem/polling/stat_entry(msg)
|
|
msg += "Active: [length(currently_polling)] | Total: [total_polls]"
|
|
var/datum/candidate_poll/soonest_to_complete = get_next_poll_to_finish()
|
|
if(soonest_to_complete)
|
|
msg += " | Next: [DisplayTimeText(soonest_to_complete.time_left())] ([length(soonest_to_complete.signed_up)] candidates)"
|
|
return ..()
|
|
|
|
///Is there a multiple of the given event type running right now?
|
|
/datum/controller/subsystem/polling/proc/duplicate_message_check(datum/candidate_poll/poll_to_check)
|
|
for(var/datum/candidate_poll/running_poll as anything in currently_polling)
|
|
if((running_poll.poll_key == poll_to_check.poll_key && running_poll != poll_to_check) && running_poll.time_left() > 0)
|
|
return TRUE
|
|
return FALSE
|
|
|
|
/datum/controller/subsystem/polling/proc/get_next_poll_to_finish()
|
|
var/lowest_time_left = INFINITY
|
|
var/next_poll_to_finish
|
|
for(var/datum/candidate_poll/poll as anything in currently_polling)
|
|
var/time_left = poll.time_left()
|
|
if(time_left >= lowest_time_left)
|
|
continue
|
|
lowest_time_left = time_left
|
|
next_poll_to_finish = poll
|
|
|
|
if(isnull(next_poll_to_finish))
|
|
return FALSE
|
|
|
|
return next_poll_to_finish
|