Files
Bubberstation/code/controllers/subsystem/ticker.dm
SkyratBot e7230e8b4a [MIRROR] Resolves is_banned_from headaches and lag (Speeds up roundstart significantly) [MDB IGNORE] (#16001)
* Resolves is_banned_from headaches and lag (Speeds up roundstart significantly) (#69376)

About The Pull Request

Just to be clear, when I refer to time here, I am not talking about cpu time. I'm talking about real time.
This doesn't significantly reduce the amount of work we do, it just removes a lot of the waiting around we need to do for db calls to finish.

Adds queuing support to sql bans, so if an ongoing ban retrieval query is active any successive ban retrieval attempts will wait for the active query to finish

This uses the number/blocking_query_timeout config option, I hope it's still valid

This system will allow us to precache ban info, in parallel (or in batches)
With this, we can avoid needing to setup all uses of is_banned_from to support parallelization or eat the cost of in-series database requests

Clients who join after initialize will now build a ban cache automatically

Those who join before init is done will be gathered by a batch query sent by a new subsystem, SSban_cache.

This means that any post initalize uses of is_banned_from are worst case by NATURE parallel (since the request is already sent, and we're just waiting for the response)

This saves a lot of headache for implementers (users) of the proc, and saves ~0.9 second from roundstart setup for each client (on /tg/station)

There's a lot of in series is_banned_from calls in there, and this nukes them. This should bring down roundstart join times significantly.

It's hard to say exactly how much, since some cases generate the ban cache at other times.
At base tho, we save about 0.9 seconds of real time per client off doing this stuff in parallel.
Why It's Good For The Game

    When I use percentages I'm speaking about cost per player

I don't like how slow roundstart feels, this kills about 66% of that. the rest is a lot of misc things. About 11% (it's actually 16%) is general mob placing which is hard to optimize. 22% is manifest generation, most of which is GetFlatIcons which REALLY do not need to be holding up the main thread of execution.

An additional 1 second is constant cost from a db query we make to tell the server we exist, which can be made async to avoid holding the proc chain.

That's it. I'm bullying someone into working on the manifest issue, so that should just leave 16% of mob placing, which is really not that bad compared to what we have now.
Changelog

cl
code: The time between the round starting and the game like, actually starting has been reduced by 66%
refactor: I've slightly changed how ban caches are generated, admins please let me know if anything goes fuckey
server: I'm using the blocking_query_timeout config. Make sure it's up to date and all.
/cl

* Resolves is_banned_from headaches and lag (Speeds up roundstart significantly)

Co-authored-by: LemonInTheDark <58055496+LemonInTheDark@users.noreply.github.com>
2022-09-01 22:11:40 -04:00

723 lines
28 KiB
Plaintext

#define ROUND_START_MUSIC_LIST "strings/round_start_sounds.txt"
SUBSYSTEM_DEF(ticker)
name = "Ticker"
init_order = INIT_ORDER_TICKER
priority = FIRE_PRIORITY_TICKER
flags = SS_KEEP_TIMING
runlevels = RUNLEVEL_LOBBY | RUNLEVEL_SETUP | RUNLEVEL_GAME
var/current_state = GAME_STATE_STARTUP //state of current round (used by process()) Use the defines GAME_STATE_* !
var/force_ending = 0 //Round was ended by admin intervention
// If true, there is no lobby phase, the game starts immediately.
var/start_immediately = FALSE
var/setup_done = FALSE //All game setup done including mode post setup and
var/datum/game_mode/mode = null
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 it's 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
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
var/real_round_start_time = 0 //SKYRAT EDIT ADDITION
var/discord_alerted = FALSE //SKYRAT EDIT - DISCORD PING SPAM PREVENTION
/datum/controller/subsystem/ticker/Initialize(timeofday)
load_mentors() // SKYRAT EDIT ADDITION - MENTORS STOPPED LOADING AUTOMATICALLY DUE TO RECENT TG CHANGES
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 = lowertext(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.config.map_name)
music += S
else if(L[2] == "rare" && L[1] == SSmapping.config.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.config.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(music.len > 1)
music -= old_login_music
for(var/S in music)
var/list/L = splittext(S,".")
if(L.len >= 2)
var/ext = lowertext(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")
login_music = pick(music)
else
login_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
return ..()
/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("<b>Welcome to [station_name()]!</b>"))
//SKYRAT EDIT START - DISCORD SPAM PREVENTION
if(!discord_alerted)
discord_alerted = TRUE
send2chat("<@&[CONFIG_GET(string/game_alert_role_id)]> New round starting on [SSmapping.config.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.", CONFIG_GET(string/chat_announce_new_game)) // Skyrat EDIT -- role pingcurrent_state = GAME_STATE_PREGAME
current_state = GAME_STATE_PREGAME
SStitle.change_title_screen() //SKYRAT EDIT ADDITION - Title screen
addtimer(CALLBACK(SStitle, /datum/controller/subsystem/title/.proc/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)
mode.process(wait * 0.1)
check_queue()
if(!roundend_check_paused && mode.check_finished(force_ending) || force_ending)
current_state = GAME_STATE_FINISHED
toggle_ooc(TRUE) // Turn it on
toggle_dooc(TRUE)
declare_completion(force_ending)
check_maprotate()
Master.SetRunLevel(RUNLEVEL_POSTGAME)
/datum/controller/subsystem/ticker/proc/setup()
to_chat(world, span_boldannounce("Starting game..."))
var/init_start = world.timeofday
mode = new /datum/game_mode/dynamic
CHECK_TICK
//Configure mode and assign player to special mode stuff
var/can_continue = 0
can_continue = src.mode.pre_setup() //Choose antagonists
CHECK_TICK
can_continue = can_continue && SSjob.DivideOccupations() //Distribute jobs
CHECK_TICK
if(!GLOB.Debug2)
if(!can_continue)
log_game("Game failed pre_setup")
QDEL_NULL(mode)
to_chat(world, "<B>Error setting up game.</B> Reverting to pre-game lobby.")
SSjob.ResetOccupations()
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.data_core.manifest()
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)
SEND_SIGNAL(src, COMSIG_TICKER_ROUND_STARTING)
real_round_start_time = world.timeofday //SKYRAT EDIT ADDITION
log_world("Game start took [(world.timeofday - init_start)/10]s")
round_start_time = world.time
INVOKE_ASYNC(SSdbcore, /datum/controller/subsystem/dbcore/proc/SetRoundStart)
to_chat(world, span_notice("<B>Welcome to [station_name()], enjoy your stay!</B>"))
alert_sound_to_playing(sound(SSstation.announcer.get_rand_welcome_sound())) //SKYRAT EDIT CHANGE
current_state = GAME_STATE_PLAYING
Master.SetRunLevel(RUNLEVEL_GAME)
if(SSevents.holidays)
to_chat(world, span_notice("and..."))
for(var/holidayname in SSevents.holidays)
var/datum/holiday/holiday = SSevents.holidays[holidayname]
to_chat(world, "<h4>[holiday.greet()]</h4>")
PostSetup()
return TRUE
/datum/controller/subsystem/ticker/proc/PostSetup()
set waitfor = FALSE
mode.post_setup()
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()
if(!iter_human.hardcore_survival_score)
continue
if(iter_human.mind?.special_role)
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!"))
//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/station_explosion_detonation(atom/bomb)
if(bomb) //BOOM
qdel(bomb)
/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.EquipRank(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, .proc/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)
//SKYRAT EDIT ADDITION
if(ishuman(new_player_living))
for(var/datum/loadout_item/item as anything in loadout_list_to_datums(new_player_mob.client?.prefs?.loadout_list))
if (item.restricted_roles && length(item.restricted_roles) && !(player_assigned_role.title in item.restricted_roles))
continue
item.post_equip_item(new_player_mob.client?.prefs, new_player_living)
//SKYRAT EDIT END
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/i in GLOB.new_player_list)
var/mob/dead/new_player/player = i
var/mob/living = player.transfer_character()
if(living)
qdel(player)
living.notransform = TRUE
if(living.client)
var/atom/movable/screen/splash/S = new(null, living.client, TRUE)
S.Fade(TRUE)
living.client.init_verbs()
livings += living
if(livings.len)
addtimer(CALLBACK(src, .proc/release_characters, livings), 30, TIMER_CLIENT_TIME)
/datum/controller/subsystem/ticker/proc/release_characters(list/livings)
for(var/I in livings)
var/mob/living/L = I
L.notransform = FALSE
/datum/controller/subsystem/ticker/proc/check_queue()
if(!queued_players.len)
return
var/hpc = CONFIG_GET(number/hard_popcap)
if(!hpc)
list_clear_nulls(queued_players)
for (var/mob/dead/new_player/NP in queued_players)
to_chat(NP, span_userdanger("The alive players limit has been released!<br><a href='?src=[REF(NP)];late_join=override'>[html_encode(">>Join Game<<")]</a>"))
SEND_SOUND(NP, sound('sound/misc/notice1.ogg'))
NP.LateChoices()
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() < hpc)
if(next_in_line?.client)
to_chat(next_in_line, span_userdanger("A slot has opened! You have approximately 20 seconds to join. <a href='?src=[REF(next_in_line)];late_join=override'>\>\>Join Game\<\<</a>"))
SEND_SOUND(next_in_line, sound('sound/misc/notice1.ogg'))
next_in_line.LateChoices()
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
/datum/controller/subsystem/ticker/proc/check_maprotate()
if(!CONFIG_GET(flag/maprotation))
return
if(world.time - SSticker.round_start_time < 10 MINUTES) //Not forcing map rotation for very short rounds.
return
INVOKE_ASYNC(SSmapping, /datum/controller/subsystem/mapping/.proc/maprotate)
/datum/controller/subsystem/ticker/proc/HasRoundStarted()
return current_state >= GAME_STATE_PLAYING
/datum/controller/subsystem/ticker/proc/IsRoundInProgress()
return current_state == GAME_STATE_PLAYING
/datum/controller/subsystem/ticker/Recover()
current_state = SSticker.current_state
force_ending = SSticker.force_ending
mode = SSticker.mode
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)]([station_name()])") //decode station_name to avoid minor_announce double encode //SKYRAT EDIT CHANGE
switch(news_report)
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."
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!"
if(STATION_EVACUATED)
if(emergency_reason)
news_message = "[decoded_station_name] has been evacuated after transmitting the following distress beacon:\n\n[html_decode(emergency_reason)]"
else
news_message = "The crew of [decoded_station_name] has been evacuated amid unconfirmed reports of enemy activity."
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."
if(BLOB_NUKE)
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."
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."
if(CULT_ESCAPE)
news_message = "Security Alert: A group of religious fanatics have escaped from [decoded_station_name]."
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."
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."
if(NUKE_MISS)
news_message = "The Syndicate have bungled a terrorist attack [decoded_station_name], detonating a nuclear weapon in empty space nearby."
if(OPERATIVES_KILLED)
news_message = "Repairs to [decoded_station_name] are underway after an elite Syndicate death squad was wiped out by the crew."
if(OPERATIVE_SKIRMISH)
news_message = "A skirmish between security forces and Syndicate agents aboard [decoded_station_name] ended with both sides bloodied but intact."
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."
if(REVS_LOSE)
news_message = "[decoded_station_name] quickly put down a misguided attempt at mutiny. Remember, unionizing is illegal!"
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]."
if(STATION_NUKED)
news_message = "[decoded_station_name] activated its self-destruct device for unknown reasons. Attempts to clone the Captain for arrest and execution are underway."
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."
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/start_wait = world.time
UNTIL(round_end_sound_sent || (world.time - start_wait) > (delay * 2)) //don't wait forever
sleep(delay - (world.time - start_wait))
if(delay_end && !skip_delay)
to_chat(world, span_boldannounce("Reboot was cancelled by an admin."))
return
if(end_string)
end_state = end_string
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 <a href=\"[statspage][GLOB.round_id]\">at this website!</a>"))
else if(gamelogloc)
to_chat(world, span_info("Round logs can be located <a href=\"[gamelogloc]\">at this website!</a>"))
log_game(span_boldannounce("Rebooting World. [reason]"))
world.Reboot()
/datum/controller/subsystem/ticker/Shutdown()
gather_newscaster() //called here so we ensure the log is created even upon admin reboot
save_admin_data()
update_everything_flag_in_db()
if(!round_end_sound)
round_end_sound = choose_round_end_song()
///The reference to the end of round sound that we have chosen.
var/sound/end_of_round_sound_ref = sound(round_end_sound)
for(var/mob/M in GLOB.player_list)
if(M.client.prefs?.toggles & SOUND_ENDOFROUND)
SEND_SOUND(M.client, end_of_round_sound_ref)
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)]"