#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.
#ifdef ABSOLUTE_MINIMUM
var/start_immediately = TRUE
#else
var/start_immediately = FALSE
#endif
/// 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 = 2 //factor of station time progressal vs real time. // BUBBER EDIT CHANGE - SERVER TIME OFFSET - ORIGINAL: 12
var/server_time_offset // Offset between server time and station time // BUBBER EDIT ADDITION - SERVER TIME OFFSET
/// 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/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)
if(IS_SOUND_FILE(S))
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) * (1 SECONDS))
round_start_time = start_at // May be changed later, but prevents the time from jumping back when the round actually starts
server_time_offset = (CONFIG_GET(number/shift_time_clock_offset) * (1 MINUTES)) // BUBBER EDIT ADD: SERVER TIME OFFSET
if(CONFIG_GET(flag/randomize_shift_time))
gametime_offset = rand(0, 23) * (1 HOURS)
else if(CONFIG_GET(flag/shift_time_realtime))
gametime_offset = world.timeofday + GLOB.timezoneOffset + server_time_offset // BUBBER EDIT CHANGE - SERVER TIME OFFSET - ORIGINAL: gametime_offset = world.timeofday + GLOB.timezoneOffset
// station_time_rate_multiplier = 1 // BUBBER EDIT REMOVAL
else
gametime_offset = (CONFIG_GET(number/shift_time_start_hour) * (1 HOURS))
// BUBBER EDIT ADD BEGIN - SERVER TIME OFFSET
message_admins("Station time set to [station_time_timestamp(format = "hh:mm")]. Night shift transitions are [SSnightshift.can_fire ? span_vote_notice("enabled") : span_comradio("disabled")].")
log_dynamic("Station time set to [station_time_timestamp(format = "hh:mm")]. Server time: [time2text(world.timeofday, "hh:mm", world.timezone)]. Server time offset: [server_time_offset / (1 MINUTES)] min. Night shift transitions are [SSnightshift.can_fire ? "enabled" : "disabled"].")
// BUBBER EDIT ADD END - SERVER TIME OFFSET
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("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
var/list/readied_players = list() //BUBBER EDIT ADDITION
for(var/mob/dead/new_player/player as anything in GLOB.new_player_list)
if(player.ready == PLAYER_READY_TO_PLAY)
readied_players[player.key] = player //BUBBER EDIT: job estimation, filling the readied list we use later
++totalPlayersReady
if(player.client?.holder)
++total_admins_ready
job_estimation_list = get_job_estimation(readied_players) //BUBBER EDIT ADDITION
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
/// Gets a list of players with their readied state so we can post it as a log
/datum/controller/subsystem/ticker/proc/get_player_ready_states()
var/list/player_states = list()
for(var/mob/dead/new_player/player as anything in GLOB.new_player_list)
player_states[player.ckey] = player.ready
return player_states
/datum/controller/subsystem/ticker/proc/setup()
to_chat(world, span_boldannounce("Starting game..."))
var/init_start = world.timeofday
var/list/players_and_readiness = get_player_ready_states()
log_game("Players and Readiness: [json_encode(players_and_readiness)]", players_and_readiness)
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.debugging_enabled)
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
var/roundstart_logout_timer = CONFIG_GET(number/roundstart_logout_report_time_average)
var/roundstart_report_variance = CONFIG_GET(number/roundstart_logout_report_time_variance)
var/randomized_callback_timer = rand((roundstart_logout_timer - roundstart_report_variance), (roundstart_logout_timer + roundstart_report_variance))
addtimer(CALLBACK(src, PROC_REF(display_roundstart_logout_report)), randomized_callback_timer)
GLOB.logout_timer_set = randomized_callback_timer
// 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 >= GLOB.logout_timer_set) //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
msg += "[span_boldnotice("Roundstart logout reported at: [DisplayTimeText(GLOB.logout_timer_set)]")]\n"
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