mirror of
https://github.com/Bubberstation/Bubberstation.git
synced 2026-01-26 00:51:23 +00:00
## About The Pull Request Sorta-config related, replaced magic numbers in station time code with defines and fixed station time calculations. Constant time was (incorrectly) adjusted by your timezone in actual station time calculations despite us changing all time ops to UTC+0, and realtime was adjusted by your timezone in the wrong direction (realtime returns GMT, and station time itself deducted your timezone again). Also fixed station_time_debug debug proc using seconds instead of deciseconds (by converting it to defines) I've also made realtime config change time dilation factor from 12 to 1, as its intended to make station time reflect real time *in the server's own timezone*, and 12x time dilation made it deviate from that extremely quickly (this does not affect gameplay in any meaningful manner, station time dilation only affects displayed clock time and not any in-game events except nightshifts) - Closes #92380 ## Changelog 🆑 fix: Fixed constant and realtime station time being calculated incorrectly /🆑 --------- Co-authored-by: LT3 <83487515+lessthnthree@users.noreply.github.com>
884 lines
34 KiB
Plaintext
884 lines
34 KiB
Plaintext
#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
|
|
|
|
/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) * (1 SECONDS))
|
|
round_start_time = start_at // May be changed later, but prevents the time from jumping back when the round actually starts
|
|
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
|
|
station_time_rate_multiplier = 1
|
|
else
|
|
gametime_offset = (CONFIG_GET(number/shift_time_start_hour) * (1 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("<b>Welcome to [station_name()]!</b>"))
|
|
for(var/channel_tag in CONFIG_GET(str_list/channel_announce_new_game))
|
|
send2chat(new /datum/tgs_message_content("New round starting on [SSmapping.current_map.map_name]!"), channel_tag)
|
|
current_state = GAME_STATE_PREGAME
|
|
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)
|
|
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
|
|
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, "<B>Error setting up game.</B> 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)
|
|
|
|
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
|
|
if(!CONFIG_GET(flag/no_intercept_report))
|
|
GLOB.communications_controller.queue_roundstart_report()
|
|
// 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)
|
|
|
|
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.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 += "<b>[L.name]</b> ([L.key]), the [L.job] (<font color='#ffcc00'><b>Disconnected</b></font>)\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 += "<b>[L.name]</b> ([L.key]), the [L.job] (<font color='#ffcc00'><b>Connected, Inactive</b></font>)\n"
|
|
failed = TRUE //AFK client
|
|
if(!failed && L.stat)
|
|
if(HAS_TRAIT(L, TRAIT_SUICIDED)) //Suicider
|
|
msg += "<b>[L.name]</b> ([L.key]), the [L.job] ([span_bolddanger("Suicide")])\n"
|
|
failed = TRUE //Disconnected client
|
|
if(!failed && (L.stat == UNCONSCIOUS || L.stat == HARD_CRIT))
|
|
msg += "<b>[L.name]</b> ([L.key]), the [L.job] (Dying)\n"
|
|
failed = TRUE //Unconscious
|
|
if(!failed && L.stat == DEAD)
|
|
msg += "<b>[L.name]</b> ([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 += "<b>[L.name]</b> ([ckey(D.mind.key)]), the [L.job] ([span_bolddanger("Suicide")])\n"
|
|
continue //Disconnected client
|
|
else
|
|
msg += "<b>[L.name]</b> ([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 += "<b>[L.name]</b> ([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 = {"
|
|
<font size = 3><b>[command_name()] Human Resources Board</b><br>
|
|
Notice of Personnel Change</font><hr>
|
|
To personnel management staff aboard [station_name()]:<br><br>
|
|
Our medical staff have detected a series of anomalies in the vital sensors
|
|
of some of the staff aboard your station.<br><br>
|
|
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.<br><br>
|
|
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.)<br><br>
|
|
<i>The following positions have been reopened on our behalf:<br><br>
|
|
[reopened_job_report_positions]</i>
|
|
"}
|
|
|
|
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.
|
|
continue
|
|
player.create_character(destination)
|
|
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!<br><a href='byond://?src=[REF(new_player)];late_join=override'>[html_encode(">>Join Game<<")]</a>"))
|
|
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. <a href='byond://?src=[REF(next_in_line)];late_join=override'>\>\>Join Game\<\<</a>"))
|
|
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(station_name()) //decode station_name to avoid minor_announce double encode
|
|
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]."
|
|
|
|
if(news_message)
|
|
send2otherserver(news_source, news_message, "News_Report")
|
|
|
|
/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 <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>"))
|
|
|
|
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
|