#define ROUND_START_MUSIC_LIST "strings/round_start_sounds.txt" #define SS_TICKER_TRAIT "SS_Ticker" SUBSYSTEM_DEF(ticker) name = "Ticker" priority = FIRE_PRIORITY_TICKER flags = SS_KEEP_TIMING runlevels = RUNLEVEL_LOBBY | RUNLEVEL_SETUP | RUNLEVEL_GAME /// state of current round (used by process()) Use the defines GAME_STATE_* ! var/current_state = GAME_STATE_STARTUP /// Boolean to track if round should be forcibly ended next ticker tick. /// Set by admin intervention ([ADMIN_FORCE_END_ROUND]) /// or a "round-ending" event, like summoning Nar'Sie, a blob victory, the nuke going off, etc. ([FORCE_END_ROUND]) var/force_ending = END_ROUND_AS_NORMAL /// If TRUE, there is no lobby phase, the game starts immediately. var/start_immediately = FALSE /// Boolean to track and check if our subsystem setup is done. var/setup_done = FALSE var/login_music //music played in pregame lobby var/round_end_sound //music/jingle played when the world reboots var/round_end_sound_sent = TRUE //If all clients have loaded it var/list/datum/mind/minds = list() //The characters in the game. Used for objective tracking. var/delay_end = FALSE //if set true, the round will not restart on its own var/admin_delay_notice = "" //a message to display to anyone who tries to restart the world after a delay var/ready_for_reboot = FALSE //all roundend preparation done with, all that's left is reboot var/tipped = FALSE //Did we broadcast the tip of the day yet? var/selected_tip // What will be the tip of the day? var/timeLeft //pregame timer var/start_at var/gametime_offset = 432000 //Deciseconds to add to world.time for station time. var/station_time_rate_multiplier = 12 //factor of station time progressal vs real time. /// Num of players, used for pregame stats on statpanel var/totalPlayers = 0 /// Num of ready players, used for pregame stats on statpanel (only viewable by admins) var/totalPlayersReady = 0 /// Num of ready admins, used for pregame stats on statpanel (only viewable by admins) var/total_admins_ready = 0 var/queue_delay = 0 var/list/queued_players = list() //used for join queues when the server exceeds the hard population cap /// What is going to be reported to other stations at end of round? var/news_report var/roundend_check_paused = FALSE var/round_start_time = 0 var/list/round_start_events var/list/round_end_events var/mode_result = "undefined" var/end_state = "undefined" /// People who have been commended and will receive a heart var/list/hearts /// Why an emergency shuttle was called var/emergency_reason /// ID of round reboot timer, if it exists var/reboot_timer = null var/real_round_start_time = 0 //SKYRAT EDIT ADDITION var/discord_alerted = FALSE //SKYRAT EDIT - DISCORD PING SPAM PREVENTION /datum/controller/subsystem/ticker/Initialize() var/list/byond_sound_formats = list( "mid" = TRUE, "midi" = TRUE, "mod" = TRUE, "it" = TRUE, "s3m" = TRUE, "xm" = TRUE, "oxm" = TRUE, "wav" = TRUE, "ogg" = TRUE, "raw" = TRUE, "wma" = TRUE, "aiff" = TRUE, ) var/list/provisional_title_music = flist("[global.config.directory]/title_music/sounds/") var/list/music = list() var/use_rare_music = prob(1) for(var/S in provisional_title_music) var/lower = LOWER_TEXT(S) var/list/L = splittext(lower,"+") switch(L.len) if(3) //rare+MAP+sound.ogg or MAP+rare.sound.ogg -- Rare Map-specific sounds if(use_rare_music) if(L[1] == "rare" && L[2] == SSmapping.current_map.map_name) music += S else if(L[2] == "rare" && L[1] == SSmapping.current_map.map_name) music += S if(2) //rare+sound.ogg or MAP+sound.ogg -- Rare sounds or Map-specific sounds if((use_rare_music && L[1] == "rare") || (L[1] == SSmapping.current_map.map_name)) music += S if(1) //sound.ogg -- common sound if(L[1] == "exclude") continue music += S var/old_login_music = trim(file2text("data/last_round_lobby_music.txt")) if(length(music) > 1) music -= old_login_music for(var/S in music) var/list/L = splittext(S,".") if(L.len >= 2) var/ext = LOWER_TEXT(L[L.len]) //pick the real extension, no 'honk.ogg.exe' nonsense here if(byond_sound_formats[ext]) continue music -= S if(!length(music)) music = world.file2list(ROUND_START_MUSIC_LIST, "\n") if(length(music) > 1) music -= old_login_music set_lobby_music(pick(music)) else set_lobby_music("[global.config.directory]/title_music/sounds/[pick(music)]") if(!GLOB.syndicate_code_phrase) GLOB.syndicate_code_phrase = generate_code_phrase(return_list=TRUE) var/codewords = jointext(GLOB.syndicate_code_phrase, "|") var/regex/codeword_match = new("([codewords])", "ig") GLOB.syndicate_code_phrase_regex = codeword_match if(!GLOB.syndicate_code_response) GLOB.syndicate_code_response = generate_code_phrase(return_list=TRUE) var/codewords = jointext(GLOB.syndicate_code_response, "|") var/regex/codeword_match = new("([codewords])", "ig") GLOB.syndicate_code_response_regex = codeword_match start_at = world.time + (CONFIG_GET(number/lobby_countdown) * 10) if(CONFIG_GET(flag/randomize_shift_time)) gametime_offset = rand(0, 23) HOURS else if(CONFIG_GET(flag/shift_time_realtime)) gametime_offset = world.timeofday else gametime_offset = (CONFIG_GET(number/shift_time_start_hour) HOURS) return SS_INIT_SUCCESS /datum/controller/subsystem/ticker/fire() switch(current_state) if(GAME_STATE_STARTUP) if(Master.initializations_finished_with_no_players_logged_in) start_at = world.time + (CONFIG_GET(number/lobby_countdown) * 10) for(var/client/C in GLOB.clients) window_flash(C, ignorepref = TRUE) //let them know lobby has opened up. to_chat(world, span_notice("Welcome to [station_name()]!")) for(var/channel_tag in CONFIG_GET(str_list/channel_announce_new_game)) // BUBBER EDIT CHANGE BEGIN - Replace with more rich message // send2chat(new /datum/tgs_message_content("New round starting on [SSmapping.current_map.map_name]!"), channel_tag) send2chat(new /datum/tgs_message_content("<@&[CONFIG_GET(string/game_alert_role_id)]> Round **[GLOB.round_id]** starting on [SSmapping.current_map.map_name], [CONFIG_GET(string/servername)]! \nIf you wish to be pinged for game related stuff, go to <#[CONFIG_GET(string/role_assign_channel_id)]> and assign yourself the roles."), channel_tag) // BUBBER EDIT CHANGE END - Replace with more rich message current_state = GAME_STATE_PREGAME // BUBBERSTATION EDIT START var/storyteller = CONFIG_GET(string/default_storyteller) if(storyteller) SSgamemode.set_storyteller(text2path(storyteller), TRUE) else SSvote.initiate_vote(/datum/vote/storyteller, "Storyteller Vote", forced = TRUE) // BUBBERSTATION EDIT END SStitle.change_title_screen() //SKYRAT EDIT ADDITION - Title screen addtimer(CALLBACK(SStitle, TYPE_PROC_REF(/datum/controller/subsystem/title, change_title_screen)), 1 SECONDS) //SKYRAT EDIT ADDITION - Title screen //Everyone who wants to be an observer is now spawned SEND_SIGNAL(src, COMSIG_TICKER_ENTER_PREGAME) fire() if(GAME_STATE_PREGAME) //lobby stats for statpanels if(isnull(timeLeft)) timeLeft = max(0,start_at - world.time) totalPlayers = LAZYLEN(GLOB.new_player_list) totalPlayersReady = 0 total_admins_ready = 0 for(var/mob/dead/new_player/player as anything in GLOB.new_player_list) if(player.ready == PLAYER_READY_TO_PLAY) ++totalPlayersReady if(player.client?.holder) ++total_admins_ready if(start_immediately) timeLeft = 0 //countdown if(timeLeft < 0) return timeLeft -= wait if(timeLeft <= 300 && !tipped) send_tip_of_the_round(world, selected_tip) tipped = TRUE if(timeLeft <= 0) SEND_SIGNAL(src, COMSIG_TICKER_ENTER_SETTING_UP) current_state = GAME_STATE_SETTING_UP Master.SetRunLevel(RUNLEVEL_SETUP) SSevents.reschedule() // SKYRAT EDIT ADDITION if(start_immediately) fire() if(GAME_STATE_SETTING_UP) if(!setup()) //setup failed current_state = GAME_STATE_STARTUP start_at = world.time + (CONFIG_GET(number/lobby_countdown) * 10) timeLeft = null Master.SetRunLevel(RUNLEVEL_LOBBY) SEND_SIGNAL(src, COMSIG_TICKER_ERROR_SETTING_UP) if(GAME_STATE_PLAYING) check_queue() if(!roundend_check_paused && (check_finished() || force_ending)) current_state = GAME_STATE_FINISHED toggle_ooc(TRUE) // Turn it on toggle_dooc(TRUE) declare_completion(force_ending) Master.SetRunLevel(RUNLEVEL_POSTGAME) /// Checks if the round should be ending, called every ticker tick /datum/controller/subsystem/ticker/proc/check_finished() if(!setup_done) return FALSE if(SSshuttle.emergency && (SSshuttle.emergency.mode == SHUTTLE_ENDGAME)) return TRUE if(GLOB.station_was_nuked) return TRUE if(GLOB.revolution_handler?.result == REVOLUTION_VICTORY) return TRUE return FALSE /datum/controller/subsystem/ticker/proc/setup() to_chat(world, span_boldannounce("Starting game...")) var/init_start = world.timeofday CHECK_TICK //Configure mode and assign player to antagonists var/can_continue = FALSE // can_continue = SSdynamic.select_roundstart_antagonists() //Choose antagonists // BUBBER EDIT - STORYTELLER (note: maybe disable) //BUBBER EDIT BEGIN - STORYTELLER SSgamemode.init_storyteller() can_continue = SSgamemode.pre_setup() //BUBBER EDIT END - STORYTELLER CHECK_TICK SEND_GLOBAL_SIGNAL(COMSIG_GLOB_PRE_JOBS_ASSIGNED, src) can_continue = can_continue && SSjob.divide_occupations() //Distribute jobs CHECK_TICK if(!GLOB.Debug2) if(!can_continue) log_game("Game failed pre_setup") to_chat(world, "Error setting up game. Reverting to pre-game lobby.") SSjob.reset_occupations() return FALSE else message_admins(span_notice("DEBUG: Bypassing prestart checks...")) CHECK_TICK // There may be various config settings that have been set or modified by this point. // This is the point of no return before spawning in new players, let's run over the // job trim singletons and update them based on any config settings. SSid_access.refresh_job_trim_singletons() CHECK_TICK if(!CONFIG_GET(flag/ooc_during_round)) toggle_ooc(FALSE) // Turn it off CHECK_TICK GLOB.start_landmarks_list = shuffle(GLOB.start_landmarks_list) //Shuffle the order of spawn points so they dont always predictably spawn bottom-up and right-to-left create_characters() //Create player characters collect_minds() equip_characters() GLOB.manifest.build() transfer_characters() //transfer keys to the new mobs for(var/I in round_start_events) var/datum/callback/cb = I cb.InvokeAsync() LAZYCLEARLIST(round_start_events) round_start_time = world.time //otherwise round_start_time would be 0 for the signals SEND_SIGNAL(src, COMSIG_TICKER_ROUND_STARTING, world.time) real_round_start_time = REALTIMEOFDAY //SKYRAT EDIT ADDITION SSautotransfer.new_shift(real_round_start_time) //SKYRAT EDIT ADDITION log_world("Game start took [(world.timeofday - init_start)/10]s") INVOKE_ASYNC(SSdbcore, TYPE_PROC_REF(/datum/controller/subsystem/dbcore,SetRoundStart)) to_chat(world, span_notice(span_bold("Welcome to [station_name()], enjoy your stay!"))) SEND_SOUND(world, sound(SSstation.announcer.get_rand_welcome_sound())) current_state = GAME_STATE_PLAYING Master.SetRunLevel(RUNLEVEL_GAME) if(length(GLOB.holidays)) to_chat(world, span_notice("and...")) for(var/holidayname in GLOB.holidays) var/datum/holiday/holiday = GLOB.holidays[holidayname] to_chat(world, span_info(holiday.greet())) PostSetup() return TRUE /datum/controller/subsystem/ticker/proc/PostSetup() set waitfor = FALSE // Spawn traitors and stuff for(var/datum/dynamic_ruleset/roundstart/ruleset in SSdynamic.queued_rulesets) ruleset.execute() SSdynamic.queued_rulesets -= ruleset SSdynamic.executed_rulesets += ruleset // Queue roundstart intercept report /* BUBBER EDIT REMOVAL BEGIN - Storyteller if(!CONFIG_GET(flag/no_intercept_report)) GLOB.communications_controller.queue_roundstart_report() */// BUBBER EDIT REMOVAL END - Storyteller // Queue admin logout report addtimer(CALLBACK(src, PROC_REF(display_roundstart_logout_report)), ROUNDSTART_LOGOUT_REPORT_TIME) // Queue suicide slot handling if(CONFIG_GET(flag/reopen_roundstart_suicide_roles)) var/delay = (CONFIG_GET(number/reopen_roundstart_suicide_roles_delay) * 1 SECONDS) || 4 MINUTES addtimer(CALLBACK(src, PROC_REF(reopen_roundstart_suicide_roles)), delay) // Handle database if(SSdbcore.Connect()) var/list/to_set = list() var/arguments = list() if(GLOB.revdata.originmastercommit) to_set += "commit_hash = :commit_hash" arguments["commit_hash"] = GLOB.revdata.originmastercommit if(to_set.len) arguments["round_id"] = GLOB.round_id var/datum/db_query/query_round_game_mode = SSdbcore.NewQuery( "UPDATE [format_table_name("round")] SET [to_set.Join(", ")] WHERE id = :round_id", arguments ) query_round_game_mode.Execute() qdel(query_round_game_mode) SSgamemode.post_setup() // BUBBER EDIT - Storyteller GLOB.start_state = new /datum/station_state() GLOB.start_state.count() var/list/adm = get_admin_counts() var/list/allmins = adm["present"] send2adminchat("Server", "Round [GLOB.round_id ? "#[GLOB.round_id]" : ""] has started[allmins.len ? ".":" with no active admins online!"]") setup_done = TRUE for(var/i in GLOB.start_landmarks_list) var/obj/effect/landmark/start/S = i if(istype(S)) //we can not runtime here. not in this important of a proc. S.after_round_start() else stack_trace("[S] [S.type] found in start landmarks list, which isn't a start landmark!") // handle persistence stuff that requires ckeys, in this case hardcore mode and temporal scarring for(var/i in GLOB.player_list) if(!ishuman(i)) continue var/mob/living/carbon/human/iter_human = i iter_human.increment_scar_slot() iter_human.load_persistent_scars() SSpersistence.load_modular_persistence(iter_human.get_organ_slot(ORGAN_SLOT_BRAIN)) // SKYRAT EDIT ADDITION - MODULAR_PERSISTENCE iter_human.add_to_player_list() // BUBBER EDIT ADDITION - CHARACTER DIRECTORY if(!iter_human.hardcore_survival_score) continue if(iter_human.is_antag()) to_chat(iter_human, span_notice("You will gain [round(iter_human.hardcore_survival_score) * 2] hardcore random points if you greentext this round!")) else to_chat(iter_human, span_notice("You will gain [round(iter_human.hardcore_survival_score)] hardcore random points if you survive this round!")) /datum/controller/subsystem/ticker/proc/display_roundstart_logout_report() var/list/msg = list("[span_boldnotice("Roundstart logout report")]\n\n") for(var/i in GLOB.mob_living_list) var/mob/living/L = i var/mob/living/carbon/C = L if (istype(C) && !C.last_mind) continue // never had a client if(L.ckey && !GLOB.directory[L.ckey]) msg += "[L.name] ([L.key]), the [L.job] (Disconnected)\n" if(L.ckey && L.client) var/failed = FALSE if(L.client.inactivity >= ROUNDSTART_LOGOUT_AFK_THRESHOLD) //Connected, but inactive (alt+tabbed or something) msg += "[L.name] ([L.key]), the [L.job] (Connected, Inactive)\n" failed = TRUE //AFK client if(!failed && L.stat) if(HAS_TRAIT(L, TRAIT_SUICIDED)) //Suicider msg += "[L.name] ([L.key]), the [L.job] ([span_bolddanger("Suicide")])\n" failed = TRUE //Disconnected client if(!failed && (L.stat == UNCONSCIOUS || L.stat == HARD_CRIT)) msg += "[L.name] ([L.key]), the [L.job] (Dying)\n" failed = TRUE //Unconscious if(!failed && L.stat == DEAD) msg += "[L.name] ([L.key]), the [L.job] (Dead)\n" failed = TRUE //Dead continue //Happy connected client for(var/mob/dead/observer/D in GLOB.dead_mob_list) if(D.mind && D.mind.current == L) if(L.stat == DEAD) if(HAS_TRAIT(L, TRAIT_SUICIDED)) //Suicider msg += "[L.name] ([ckey(D.mind.key)]), the [L.job] ([span_bolddanger("Suicide")])\n" continue //Disconnected client else msg += "[L.name] ([ckey(D.mind.key)]), the [L.job] (Dead)\n" continue //Dead mob, ghost abandoned else if(D.can_reenter_corpse) continue //Adminghost, or cult/wizard ghost else msg += "[L.name] ([ckey(D.mind.key)]), the [L.job] ([span_bolddanger("Ghosted")])\n" continue //Ghosted while alive var/concatenated_message = msg.Join() log_admin(concatenated_message) to_chat(GLOB.admins, concatenated_message) /datum/controller/subsystem/ticker/proc/reopen_roundstart_suicide_roles() var/include_command = CONFIG_GET(flag/reopen_roundstart_suicide_roles_command_positions) var/list/reopened_jobs = list() for(var/mob/living/quitter in GLOB.suicided_mob_list) var/datum/job/job = SSjob.get_job(quitter.job) if(!job || !(job.job_flags & JOB_REOPEN_ON_ROUNDSTART_LOSS)) continue if(!include_command && job.departments_bitflags & DEPARTMENT_BITFLAG_COMMAND) continue job.current_positions = max(job.current_positions - 1, 0) reopened_jobs += quitter.job if(CONFIG_GET(flag/reopen_roundstart_suicide_roles_command_report)) if(reopened_jobs.len) var/reopened_job_report_positions for(var/dead_dudes_job in reopened_jobs) reopened_job_report_positions = "[reopened_job_report_positions ? "[reopened_job_report_positions]\n":""][dead_dudes_job]" var/suicide_command_report = {" [command_name()] Human Resources Board
Notice of Personnel Change

To personnel management staff aboard [station_name()]:

Our medical staff have detected a series of anomalies in the vital sensors of some of the staff aboard your station.

Further investigation into the situation on our end resulted in us discovering a series of rather... unforturnate decisions that were made on the part of said staff.

As such, we have taken the liberty to automatically reopen employment opportunities for the positions of the crew members who have decided not to partake in our research. We will be forwarding their cases to our employment review board to determine their eligibility for continued service with the company (and of course the continued storage of cloning records within the central medical backup server.)

The following positions have been reopened on our behalf:

[reopened_job_report_positions]
"} print_command_report(suicide_command_report, "Central Command Personnel Update") //These callbacks will fire after roundstart key transfer /datum/controller/subsystem/ticker/proc/OnRoundstart(datum/callback/cb) if(!HasRoundStarted()) LAZYADD(round_start_events, cb) else cb.InvokeAsync() //These callbacks will fire before roundend report /datum/controller/subsystem/ticker/proc/OnRoundend(datum/callback/cb) if(current_state >= GAME_STATE_FINISHED) cb.InvokeAsync() else LAZYADD(round_end_events, cb) /datum/controller/subsystem/ticker/proc/create_characters() for(var/i in GLOB.new_player_list) var/mob/dead/new_player/player = i if(player.ready == PLAYER_READY_TO_PLAY && player.mind) GLOB.joined_player_list += player.ckey var/atom/destination = player.mind.assigned_role.get_roundstart_spawn_point() if(!destination) // Failed to fetch a proper roundstart location, won't be going anywhere. player.show_title_screen() //SKYRAT EDIT CHANGE continue player.create_character(destination) else player.show_title_screen() //SKYRAT EDIT ADDITION CHECK_TICK /datum/controller/subsystem/ticker/proc/collect_minds() for(var/i in GLOB.new_player_list) var/mob/dead/new_player/P = i if(P.new_character && P.new_character.mind) SSticker.minds += P.new_character.mind CHECK_TICK /datum/controller/subsystem/ticker/proc/equip_characters() GLOB.security_officer_distribution = decide_security_officer_departments( shuffle(GLOB.new_player_list), shuffle(GLOB.available_depts), ) var/captainless = TRUE var/highest_rank = length(SSjob.chain_of_command) + 1 var/list/spare_id_candidates = list() var/mob/dead/new_player/picked_spare_id_candidate // Find a suitable player to hold captaincy. for(var/mob/dead/new_player/new_player_mob as anything in GLOB.new_player_list) if(is_banned_from(new_player_mob.ckey, list(JOB_CAPTAIN))) CHECK_TICK continue if(!ishuman(new_player_mob.new_character)) continue var/mob/living/carbon/human/new_player_human = new_player_mob.new_character if(!new_player_human.mind || is_unassigned_job(new_player_human.mind.assigned_role)) continue // Keep a rolling tally of who'll get the cap's spare ID vault code. // Check assigned_role's priority and curate the candidate list appropriately. var/player_assigned_role = new_player_human.mind.assigned_role.title var/spare_id_priority = SSjob.chain_of_command[player_assigned_role] if(spare_id_priority) if(spare_id_priority < highest_rank) spare_id_candidates.Cut() spare_id_candidates += new_player_mob highest_rank = spare_id_priority else if(spare_id_priority == highest_rank) spare_id_candidates += new_player_mob CHECK_TICK if(length(spare_id_candidates)) picked_spare_id_candidate = pick(spare_id_candidates) for(var/mob/dead/new_player/new_player_mob as anything in GLOB.new_player_list) if(QDELETED(new_player_mob) || !isliving(new_player_mob.new_character)) CHECK_TICK continue var/mob/living/new_player_living = new_player_mob.new_character if(!new_player_living.mind) CHECK_TICK continue var/datum/job/player_assigned_role = new_player_living.mind.assigned_role if(player_assigned_role.job_flags & JOB_EQUIP_RANK) SSjob.equip_rank(new_player_living, player_assigned_role, new_player_mob.client) player_assigned_role.after_roundstart_spawn(new_player_living, new_player_mob.client) if(picked_spare_id_candidate == new_player_mob) captainless = FALSE var/acting_captain = !is_captain_job(player_assigned_role) SSjob.promote_to_captain(new_player_living, acting_captain) OnRoundstart(CALLBACK(GLOBAL_PROC, GLOBAL_PROC_REF(minor_announce), player_assigned_role.get_captaincy_announcement(new_player_living))) if((player_assigned_role.job_flags & JOB_ASSIGN_QUIRKS) && ishuman(new_player_living) && CONFIG_GET(flag/roundstart_traits)) if(new_player_mob.client?.prefs?.should_be_random_hardcore(player_assigned_role, new_player_living.mind)) new_player_mob.client.prefs.hardcore_random_setup(new_player_living) SSquirks.AssignQuirks(new_player_living, new_player_mob.client) if(ishuman(new_player_living)) SEND_SIGNAL(new_player_living, COMSIG_HUMAN_CHARACTER_SETUP_FINISHED) CHECK_TICK if(captainless) for(var/mob/dead/new_player/new_player_mob as anything in GLOB.new_player_list) var/mob/living/carbon/human/new_player_human = new_player_mob.new_character if(new_player_human) to_chat(new_player_mob, span_notice("Captainship not forced on anyone.")) CHECK_TICK /datum/controller/subsystem/ticker/proc/decide_security_officer_departments( list/new_players, list/departments, ) var/list/officer_mobs = list() var/list/officer_preferences = list() for (var/mob/dead/new_player/new_player_mob as anything in new_players) var/mob/living/carbon/human/character = new_player_mob.new_character if (istype(character) && is_security_officer_job(character.mind?.assigned_role)) officer_mobs += character var/datum/client_interface/client = GET_CLIENT(new_player_mob) var/preference = client?.prefs?.read_preference(/datum/preference/choiced/security_department) officer_preferences += preference var/distribution = get_officer_departments(officer_preferences, departments) var/list/output = list() for (var/index in 1 to officer_mobs.len) output[REF(officer_mobs[index])] = distribution[index] return output /datum/controller/subsystem/ticker/proc/transfer_characters() var/list/livings = list() for(var/mob/dead/new_player/player as anything in GLOB.new_player_list) var/mob/living = player.transfer_character() if(living) qdel(player) ADD_TRAIT(living, TRAIT_NO_TRANSFORM, SS_TICKER_TRAIT) if(living.client) var/atom/movable/screen/splash/fade_out = new(null, null, living.client, TRUE) fade_out.fade(TRUE) living.client.init_verbs() livings += living if(livings.len) addtimer(CALLBACK(src, PROC_REF(release_characters), livings), 3 SECONDS, TIMER_CLIENT_TIME) /datum/controller/subsystem/ticker/proc/release_characters(list/livings) for(var/mob/living/living_mob as anything in livings) REMOVE_TRAIT(living_mob, TRAIT_NO_TRANSFORM, SS_TICKER_TRAIT) /datum/controller/subsystem/ticker/proc/check_queue() if(!queued_players.len) return var/hard_popcap = CONFIG_GET(number/hard_popcap) if(!hard_popcap) list_clear_nulls(queued_players) for (var/mob/dead/new_player/new_player in queued_players) to_chat(new_player, span_userdanger("The alive players limit has been released!
[html_encode(">>Join Game<<")]")) SEND_SOUND(new_player, sound('sound/announcer/notice/notice1.ogg')) GLOB.latejoin_menu.ui_interact(new_player) queued_players.len = 0 queue_delay = 0 return queue_delay++ var/mob/dead/new_player/next_in_line = queued_players[1] switch(queue_delay) if(5) //every 5 ticks check if there is a slot available list_clear_nulls(queued_players) if(living_player_count() < hard_popcap) if(next_in_line?.client) to_chat(next_in_line, span_userdanger("A slot has opened! You have approximately 20 seconds to join. \>\>Join Game\<\<")) SEND_SOUND(next_in_line, sound('sound/announcer/notice/notice1.ogg')) next_in_line.ui_interact(next_in_line) return queued_players -= next_in_line //Client disconnected, remove he queue_delay = 0 //No vacancy: restart timer if(25 to INFINITY) //No response from the next in line when a vacancy exists, remove he to_chat(next_in_line, span_danger("No response received. You have been removed from the line.")) queued_players -= next_in_line queue_delay = 0 ///Whether the game has started, including roundend. /datum/controller/subsystem/ticker/proc/HasRoundStarted() return current_state >= GAME_STATE_PLAYING ///Whether the game is currently in progress, excluding roundend /datum/controller/subsystem/ticker/proc/IsRoundInProgress() return current_state == GAME_STATE_PLAYING ///Whether the game is currently in progress, excluding roundend /datum/controller/subsystem/ticker/proc/IsPostgame() return current_state == GAME_STATE_FINISHED /datum/controller/subsystem/ticker/Recover() current_state = SSticker.current_state force_ending = SSticker.force_ending login_music = SSticker.login_music round_end_sound = SSticker.round_end_sound minds = SSticker.minds delay_end = SSticker.delay_end tipped = SSticker.tipped selected_tip = SSticker.selected_tip timeLeft = SSticker.timeLeft totalPlayers = SSticker.totalPlayers totalPlayersReady = SSticker.totalPlayersReady total_admins_ready = SSticker.total_admins_ready queue_delay = SSticker.queue_delay queued_players = SSticker.queued_players round_start_time = SSticker.round_start_time queue_delay = SSticker.queue_delay queued_players = SSticker.queued_players if (Master) //Set Masters run level if it exists switch (current_state) if(GAME_STATE_SETTING_UP) Master.SetRunLevel(RUNLEVEL_SETUP) if(GAME_STATE_PLAYING) Master.SetRunLevel(RUNLEVEL_GAME) if(GAME_STATE_FINISHED) Master.SetRunLevel(RUNLEVEL_POSTGAME) /datum/controller/subsystem/ticker/proc/send_news_report() var/news_message var/news_source = "Nanotrasen News Network" var/decoded_station_name = html_decode(CONFIG_GET(string/cross_comms_name)) //decode station_name to avoid minor_announce double encode // SKYRAT EDIT: CROSS COMMS CONFIG var/decoded_emergency_reason = html_decode(emergency_reason) switch(news_report) // The nuke was detonated on the syndicate recon outpost if(NUKE_SYNDICATE_BASE) news_message = "In a daring raid, the heroic crew of [decoded_station_name] \ detonated a nuclear device in the heart of a terrorist base." // The station was destroyed by nuke ops if(STATION_DESTROYED_NUKE) news_message = "We would like to reassure all employees that the reports of a Syndicate \ backed nuclear attack on [decoded_station_name] are, in fact, a hoax. Have a secure day!" // The station was evacuated (normal result) if(STATION_EVACUATED) // Had an emergency reason supplied to pass along if(emergency_reason) news_message = "[decoded_station_name] has been evacuated after transmitting \ the following distress beacon:\n\n[decoded_emergency_reason]" else news_message = "The crew of [decoded_station_name] has been \ evacuated amid unconfirmed reports of enemy activity." // A blob won if(BLOB_WIN) news_message = "[decoded_station_name] was overcome by an unknown biological outbreak, killing \ all crew on board. Don't let it happen to you! Remember, a clean work station is a safe work station." // A blob was destroyed if(BLOB_DESTROYED) news_message = "[decoded_station_name] is currently undergoing decontamination procedures \ after the destruction of a biological hazard. As a reminder, any crew members experiencing \ cramps or bloating should report immediately to security for incineration." // A certain percentage of all cultists managed to escape at the end of round if(CULT_ESCAPE) news_message = "Security Alert: A group of religious fanatics have escaped from [decoded_station_name]." // Cult was completely or almost completely wiped out if(CULT_FAILURE) news_message = "Following the dismantling of a restricted cult aboard [decoded_station_name], \ we would like to remind all employees that worship outside of the Chapel is strictly prohibited, \ and cause for termination." // Cult summoned Nar'sie if(CULT_SUMMON) news_message = "Company officials would like to clarify that [decoded_station_name] was scheduled \ to be decommissioned following meteor damage earlier this year. Earlier reports of an \ unknowable eldritch horror were made in error." // Nuke detonated, but missed the station entirely if(NUKE_MISS) news_message = "The Syndicate have bungled a terrorist attack [decoded_station_name], \ detonating a nuclear weapon in empty space nearby." // All nuke ops got killed if(OPERATIVES_KILLED) news_message = "Repairs to [decoded_station_name] are underway after an elite \ Syndicate death squad was wiped out by the crew." // Nuke ops results inconclusive - Crew escaped without the disk, or nukies were left alive, or something if(OPERATIVE_SKIRMISH) news_message = "A skirmish between security forces and Syndicate agents aboard [decoded_station_name] \ ended with both sides bloodied but intact." // Revolution victory if(REVS_WIN) news_message = "Company officials have reassured investors that despite a union led revolt \ aboard [decoded_station_name] there will be no wage increases for workers." // Revolution defeat if(REVS_LOSE) news_message = "[decoded_station_name] quickly put down a misguided attempt at mutiny. \ Remember, unionizing is illegal!" // All wizards (plus apprentices) have been killed if(WIZARD_KILLED) news_message = "Tensions have flared with the Space Wizard Federation following the death \ of one of their members aboard [decoded_station_name]." // The station was nuked generically if(STATION_NUKED) // There was a blob on board, guess it was nuked to stop it if(length(GLOB.overminds)) for(var/mob/eye/blob/overmind as anything in GLOB.overminds) if(overmind.max_count < overmind.announcement_size) continue news_message = "[decoded_station_name] is currently undergoing decontanimation after a controlled \ burst of radiation was used to remove a biological ooze. All employees were safely evacuated prior, \ and are enjoying a relaxing vacation." break // A self destruct or something else else news_message = "[decoded_station_name] activated its self-destruct device for unknown reasons. \ Attempts to clone the Captain for arrest and execution are underway." // The emergency escape shuttle was hijacked if(SHUTTLE_HIJACK) news_message = "During routine evacuation procedures, the emergency shuttle of [decoded_station_name] \ had its navigation protocols corrupted and went off course, but was recovered shortly after. \ The following distress beacon was sent prior to evacuation:\n\n[Gibberish(decoded_emergency_reason, FALSE, 8)]" // A supermatter cascade triggered if(SUPERMATTER_CASCADE) news_message = "Officials are advising nearby colonies about a newly declared exclusion zone in \ the sector surrounding [decoded_station_name]." //SKYRAT EDIT - START if(SSblackbox.first_death) var/list/ded = SSblackbox.first_death if(ded.len) news_message += " NT Sanctioned Psykers picked up faint traces of someone near the station, allegedly having had died. Their name was: [ded["name"]], [ded["role"]], at [ded["area"]].[ded["last_words"] ? " Their last words were: \"[ded["last_words"]]\"" : ""]" // " // An Extra quote and comment because highlighting goes weird else news_message += " NT Sanctioned Psykers proudly confirm reports that nobody died this shift!" //SKYRAT EDIT - END if(news_message && length(CONFIG_GET(keyed_list/cross_server))) //SKYRAT EDIT - CONFIG CHECK MOVED FROM ROUNDEND.DM news_message += " (Shift on [CONFIG_GET(string/cross_server_name)] ending!)" //SKYRAT EDIT ADDITION send2otherserver(news_source, news_message,"News_Report") //SKYRAT EDIT - START if(news_message) return news_message else return "We regret to inform you that shit be whack, yo. None of our reporters have any idea of what may or may not have gone on." //SKYRAT EDIT - END /datum/controller/subsystem/ticker/proc/GetTimeLeft() if(isnull(SSticker.timeLeft)) return max(0, start_at - world.time) return timeLeft /datum/controller/subsystem/ticker/proc/SetTimeLeft(newtime) if(newtime >= 0 && isnull(timeLeft)) //remember, negative means delayed start_at = world.time + newtime else timeLeft = newtime /datum/controller/subsystem/ticker/proc/SetRoundEndSound(the_sound) set waitfor = FALSE round_end_sound_sent = FALSE round_end_sound = fcopy_rsc(the_sound) for(var/thing in GLOB.clients) var/client/C = thing if (!C) continue C.Export("##action=load_rsc", round_end_sound) round_end_sound_sent = TRUE /datum/controller/subsystem/ticker/proc/Reboot(reason, end_string, delay) set waitfor = FALSE if(usr && !check_rights(R_SERVER, TRUE)) return if(!delay) delay = CONFIG_GET(number/round_end_countdown) * 10 var/skip_delay = check_rights() if(delay_end && !skip_delay) to_chat(world, span_boldannounce("An admin has delayed the round end.")) return to_chat(world, span_boldannounce("Rebooting World in [DisplayTimeText(delay)]. [reason]")) var/statspage = CONFIG_GET(string/roundstatsurl) var/gamelogloc = CONFIG_GET(string/gamelogurl) if(statspage) to_chat(world, span_info("Round statistics and logs can be viewed at this website!")) else if(gamelogloc) to_chat(world, span_info("Round logs can be located at this website!")) var/start_wait = world.time UNTIL(round_end_sound_sent || (world.time - start_wait) > (delay * 2)) //don't wait forever reboot_timer = addtimer(CALLBACK(src, PROC_REF(reboot_callback), reason, end_string), delay - (world.time - start_wait), TIMER_STOPPABLE) /datum/controller/subsystem/ticker/proc/reboot_callback(reason, end_string) if(end_string) end_state = end_string log_game(span_boldannounce("Rebooting World. [reason]")) world.Reboot() /** * Deletes the current reboot timer and nulls the var * * Arguments: * * user - the user that cancelled the reboot, may be null */ /datum/controller/subsystem/ticker/proc/cancel_reboot(mob/user) if(!reboot_timer) to_chat(user, span_warning("There is no pending reboot!")) return FALSE to_chat(world, span_boldannounce("An admin has delayed the round end.")) deltimer(reboot_timer) reboot_timer = null return TRUE /datum/controller/subsystem/ticker/Shutdown() gather_newscaster() //called here so we ensure the log is created even upon admin reboot if(!round_end_sound) round_end_sound = choose_round_end_song() for(var/mob/M in GLOB.player_list) var/pref_volume = M.client.prefs.read_preference(/datum/preference/numeric/volume/sound_midi) if(pref_volume > 0) SEND_SOUND(M.client, sound(round_end_sound, volume = pref_volume)) text2file(login_music, "data/last_round_lobby_music.txt") /datum/controller/subsystem/ticker/proc/choose_round_end_song() var/list/reboot_sounds = flist("[global.config.directory]/reboot_themes/") var/list/possible_themes = list() for(var/themes in reboot_sounds) possible_themes += themes if(possible_themes.len) return "[global.config.directory]/reboot_themes/[pick(possible_themes)]" /// Updates the lobby music /// Does not update if override is FALSE and login_music is already set /datum/controller/subsystem/ticker/proc/set_lobby_music(new_music, override = FALSE) if(!override && login_music) return login_music = new_music #undef ROUND_START_MUSIC_LIST #undef SS_TICKER_TRAIT