Files
Paradise/code/controllers/subsystem/SSticker.dm
Qwertytoforty c66f31bd3d Ports / reworks roundstart blurbs (#23545)
* [BEGIN_MISSION]

* ai change

* also TCOMMS

* Update location_blurbs.dm

* Apply suggestions from code review

Co-authored-by: Gaxeer <44334376+Gaxeer@users.noreply.github.com>

* fixes tabbing

---------

Co-authored-by: Gaxeer <44334376+Gaxeer@users.noreply.github.com>
2024-02-05 17:18:38 -05:00

872 lines
33 KiB
Plaintext

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
offline_implications = "The game is no longer aware of when the round ends. Immediate server restart recommended."
cpu_display = SS_CPUDISPLAY_LOW
wait = 1 SECONDS
/// Time the game should start, relative to world.time
var/round_start_time = 0
/// Time that the round started
var/time_game_started = 0
/// Default timeout for if world.Reboot() doesnt have a time specified
var/const/restart_timeout = 75 SECONDS
/// Current status of the game. See code\__DEFINES\game.dm
var/current_state = GAME_STATE_STARTUP
/// Do we want to force-start as soon as we can
var/force_start = FALSE
/// Do we want to force-end as soon as we can
var/force_ending = FALSE
/// Leave here at FALSE ! setup() will take care of it when needed for Secret mode -walter0o
var/hide_mode = FALSE
/// Our current game mode
var/datum/game_mode/mode = null
/// The current pick of lobby music played in the lobby
var/login_music
/// List of all minds in the game. Used for objective tracking
var/list/datum/mind/minds = list()
/// icon_state the chaplain has chosen for his bible
var/Bible_icon_state
/// item_state the chaplain has chosen for his bible
var/Bible_item_state
/// Name of the bible
var/Bible_name
/// Name of the bible deity
var/Bible_deity_name
/// Cult data. Here instead of cult for adminbus purposes
var/datum/cult_info/cultdat = null
/// If set to nonzero, ALL players who latejoin or declare-ready join will have random appearances/genders
var/random_players = FALSE
/// Did we broadcast the tip of the round yet?
var/tipped = FALSE
/// What will be the tip of the round?
var/selected_tip
/// This is used for calculations for the statpanel
var/pregame_timeleft
/// If set to TRUE, the round will not restart on it's own
var/delay_end = FALSE
/// Global holder for triple AI mode
var/triai = FALSE
/// Holder for inital autotransfer vote timer
var/next_autotransfer = 0
/// Used for station explosion cinematic
var/obj/screen/cinematic = null
/// Spam Prevention. Announce round end only once.
var/round_end_announced = FALSE
/// Is the ticker currently processing? If FALSE, roundstart is delayed
var/ticker_going = TRUE
/// Gamemode result (For things like cult or nukies which can end multiple ways)
var/mode_result = "undefined"
/// Server end state (Did we end properly or reboot or nuke or what)
var/end_state = "undefined"
/// Time the real reboot kicks in
var/real_reboot_time = 0
/// Datum used to generate the end of round scoreboard.
var/datum/scoreboard/score = null
/// List of ckeys who had antag rolling issues flagged
var/list/flagged_antag_rollers = list()
/datum/controller/subsystem/ticker/Initialize()
login_music = pick(\
'sound/music/thunderdome.ogg',\
'sound/music/space.ogg',\
'sound/music/title1.ogg',\
'sound/music/title2.ogg',\
'sound/music/title3.ogg',)
/datum/controller/subsystem/ticker/fire()
switch(current_state)
if(GAME_STATE_STARTUP)
// This is ran as soon as the MC starts firing, and should only run ONCE, unless startup fails
round_start_time = world.time + (GLOB.configuration.general.lobby_time SECONDS)
to_chat(world, "<B><span class='darkmblue'>Welcome to the pre-game lobby!</span></B>")
to_chat(world, "Please, setup your character and select ready. Game will start in [GLOB.configuration.general.lobby_time] seconds")
current_state = GAME_STATE_PREGAME
fire() // TG says this is a good idea
for(var/mob/new_player/N in GLOB.player_list)
if(N.client)
N.new_player_panel_proc() // to enable the observe option
if(GAME_STATE_PREGAME)
if(!SSticker.ticker_going) // This has to be referenced like this, and I dont know why. If you dont put SSticker. it will break
return
// This is so we dont have sleeps in controllers, because that is a bad, bad thing
if(!delay_end)
pregame_timeleft = max(0, round_start_time - world.time) // Normal lobby countdown when roundstart was not delayed
else
pregame_timeleft = max(0, pregame_timeleft - wait) // If roundstart was delayed, we should resume the countdown where it left off
if(pregame_timeleft <= 600 && !tipped) // 60 seconds
send_tip_of_the_round()
tipped = TRUE
if(pregame_timeleft <= 0 || force_start)
current_state = GAME_STATE_SETTING_UP
Master.SetRunLevel(RUNLEVEL_SETUP)
if(GAME_STATE_SETTING_UP)
if(!setup()) // Setup failed
current_state = GAME_STATE_STARTUP
Master.SetRunLevel(RUNLEVEL_LOBBY)
if(GAME_STATE_PLAYING)
delay_end = FALSE // reset this in case round start was delayed
mode.process()
if(world.time > next_autotransfer)
SSvote.start_vote(new /datum/vote/crew_transfer)
next_autotransfer = world.time + GLOB.configuration.vote.autotransfer_interval_time
var/game_finished = SSshuttle.emergency.mode >= SHUTTLE_ENDGAME || mode.station_was_nuked
if(GLOB.configuration.gamemode.disable_certain_round_early_end)
mode.check_finished() // some modes contain var-changing code in here, so call even if we don't uses result
else
game_finished |= mode.check_finished()
if(game_finished || force_ending)
current_state = GAME_STATE_FINISHED
if(GAME_STATE_FINISHED)
if(SSshuttle.emergency.mode >= SHUTTLE_ENDGAME && !mode.station_was_nuked)
event_blackbox(outcome = ROUND_END_CREW_TRANSFER)
current_state = GAME_STATE_FINISHED
Master.SetRunLevel(RUNLEVEL_POSTGAME) // This shouldnt process more than once, but you never know
auto_toggle_ooc(TRUE) // Turn it on
declare_completion()
addtimer(CALLBACK(src, PROC_REF(call_reboot)), 5 SECONDS)
// Start a map vote IF
// - Map rotate doesnt have a mode for today and map voting is enabled
// - Map rotate has a mode for the day and it ISNT full random
if(((!SSmaprotate.setup_done) && GLOB.configuration.vote.enable_map_voting) || (SSmaprotate.setup_done && (SSmaprotate.rotation_mode != MAPROTATION_MODE_FULL_RANDOM)))
SSvote.start_vote(new /datum/vote/map)
else
// Pick random map
var/list/pickable_types = list()
for(var/x in subtypesof(/datum/map))
var/datum/map/M = x
if(initial(M.voteable) && length(GLOB.clients) >= initial(M.min_players_random))
pickable_types += M
var/datum/map/target_map = pick(pickable_types)
SSmapping.next_map = new target_map
to_chat(world, "<span class='interface'>Map for next round: [SSmapping.next_map.fluff_name] ([SSmapping.next_map.technical_name])</span>")
/datum/controller/subsystem/ticker/proc/call_reboot()
if(mode.station_was_nuked)
reboot_helper("Station destroyed by Nuclear Device.", "nuke")
else
reboot_helper("Round ended.", "proper completion")
/datum/controller/subsystem/ticker/proc/setup()
cultdat = setupcult()
score = new()
// Create and announce mode
if(GLOB.master_mode == "secret")
hide_mode = TRUE
var/list/datum/game_mode/runnable_modes
if(GLOB.master_mode == "random" || GLOB.master_mode == "secret")
runnable_modes = GLOB.configuration.gamemode.get_runnable_modes()
if(!length(runnable_modes))
to_chat(world, "<B>Unable to choose playable game mode.</B> Reverting to pre-game lobby.")
force_start = FALSE
current_state = GAME_STATE_PREGAME
Master.SetRunLevel(RUNLEVEL_LOBBY)
return FALSE
if(GLOB.secret_force_mode != "secret")
var/datum/game_mode/M = GLOB.configuration.gamemode.pick_mode(GLOB.secret_force_mode)
if(M.can_start())
mode = GLOB.configuration.gamemode.pick_mode(GLOB.secret_force_mode)
SSjobs.ResetOccupations()
if(!mode)
mode = pickweight(runnable_modes)
if(mode)
var/mtype = mode.type
mode = new mtype
else
mode = GLOB.configuration.gamemode.pick_mode(GLOB.master_mode)
if(!mode.can_start())
to_chat(world, "<B>Unable to start [mode.name].</B> Not enough players, [mode.required_players] players needed. Reverting to pre-game lobby.")
mode = null
current_state = GAME_STATE_PREGAME
force_start = FALSE
SSjobs.ResetOccupations()
Master.SetRunLevel(RUNLEVEL_LOBBY)
return FALSE
// Randomise characters now. This avoids rare cases where a human is set as a changeling then they randomise to an IPC
for(var/mob/new_player/player in GLOB.player_list)
if(player.client.prefs.toggles2 & PREFTOGGLE_2_RANDOMSLOT)
player.client.prefs.load_random_character_slot(player.client)
// Lets check if people who ready should or shouldnt be
for(var/mob/new_player/P in GLOB.player_list)
// Not logged in
if(!P.client)
continue
// Not ready
if(!P.ready)
continue
// Not set to return if nothing available
if(P.client.prefs.active_character.alternate_option != RETURN_TO_LOBBY)
continue
var/has_antags = (length(P.client.prefs.be_special) > 0)
if(!P.client.prefs.active_character.check_any_job())
to_chat(P, "<span class='danger'>You have no jobs enabled, along with return to lobby if job is unavailable. This makes you ineligible for any round start role, please update your job preferences.</span>")
if(has_antags)
// We add these to a list so we can deal with them as a batch later
// A lot of DB tracking stuff needs doing, so we may as well async it
flagged_antag_rollers |= P.ckey
P.ready = FALSE
//Configure mode and assign player to special mode stuff
mode.pre_pre_setup()
var/can_continue = FALSE
can_continue = mode.pre_setup() //Setup special modes. This also does the antag fishing checks.
if(!can_continue)
QDEL_NULL(mode)
to_chat(world, "<B>Error setting up [GLOB.master_mode].</B> Reverting to pre-game lobby.")
current_state = GAME_STATE_PREGAME
force_start = FALSE
SSjobs.ResetOccupations()
Master.SetRunLevel(RUNLEVEL_LOBBY)
return FALSE
// Enable highpop slots just before we distribute jobs.
var/playercount = length(GLOB.clients)
var/highpop_trigger = 80
if(playercount >= highpop_trigger)
log_debug("Playercount: [playercount] versus trigger: [highpop_trigger] - loading highpop job config")
SSjobs.LoadJobs(TRUE)
else
log_debug("Playercount: [playercount] versus trigger: [highpop_trigger] - keeping standard job config")
SSjobs.DivideOccupations() //Distribute jobs
if(hide_mode)
var/list/modes = new
for(var/datum/game_mode/M in runnable_modes)
modes += M.name
modes = sortList(modes)
to_chat(world, "<B>The current game mode is - Secret!</B>")
to_chat(world, "<B>Possibilities:</B> [english_list(modes)]")
else
mode.announce()
// Behold, a rough way of figuring out what takes 10 years
var/watch = start_watch()
create_characters() // Create player characters and transfer clients
log_debug("Creating characters took [stop_watch(watch)]s")
// Gather everyones minds
for(var/mob/living/player in GLOB.player_list)
if(player.mind)
minds += player.mind
watch = start_watch()
equip_characters() // Apply outfits and loadouts to the characters
log_debug("Equipping characters took [stop_watch(watch)]s")
watch = start_watch()
GLOB.data_core.manifest() // Create the manifest
log_debug("Manifest creation took [stop_watch(watch)]s")
SEND_SIGNAL(src, COMSIG_TICKER_ROUND_STARTING, world.time)
// Update the MC and state to game playing
current_state = GAME_STATE_PLAYING
Master.SetRunLevel(RUNLEVEL_GAME)
// Generate the list of empty playable AI cores in the world
for(var/obj/effect/landmark/start/ai/A in GLOB.landmarks_list)
if(locate(/mob/living) in get_turf(A))
continue
GLOB.empty_playable_ai_cores += new /obj/structure/AIcore/deactivated(get_turf(A))
// Setup pregenerated newsfeeds
setup_news_feeds()
// Generate code phrases and responses
if(!GLOB.syndicate_code_phrase)
var/temp_syndicate_code_phrase = generate_code_phrase(return_list = TRUE)
var/codewords = jointext(temp_syndicate_code_phrase, "|")
var/regex/codeword_match = new("([codewords])", "ig")
GLOB.syndicate_code_phrase_regex = codeword_match
temp_syndicate_code_phrase = jointext(temp_syndicate_code_phrase, ", ")
GLOB.syndicate_code_phrase = temp_syndicate_code_phrase
if(!GLOB.syndicate_code_response)
var/temp_syndicate_code_response = generate_code_phrase(return_list = TRUE)
var/codewords = jointext(temp_syndicate_code_response, "|")
var/regex/codeword_match = new("([codewords])", "ig")
GLOB.syndicate_code_response_regex = codeword_match
temp_syndicate_code_response = jointext(temp_syndicate_code_response, ", ")
GLOB.syndicate_code_response = temp_syndicate_code_response
// Run post setup stuff
mode.post_setup()
// Delete starting landmarks (not AI ones because we need those for AI-ize)
for(var/obj/effect/landmark/start/S in GLOB.landmarks_list)
if(!istype(S, /obj/effect/landmark/start/ai))
qdel(S)
SSdbcore.SetRoundStart()
to_chat(world, "<span class='darkmblue'><B>Enjoy the game!</B></span>")
SEND_SOUND(world, sound('sound/AI/welcome.ogg'))
if(SSholiday.holidays)
to_chat(world, "<span class='darkmblue'>and...</span>")
for(var/holidayname in SSholiday.holidays)
var/datum/holiday/holiday = SSholiday.holidays[holidayname]
to_chat(world, "<h4>[holiday.greet()]</h4>")
GLOB.discord_manager.send2discord_simple_noadmins("**\[Info]** Round has started")
auto_toggle_ooc(FALSE) // Turn it off
time_game_started = world.time
// Sets the auto shuttle vote to happen after the config duration
next_autotransfer = world.time + GLOB.configuration.vote.autotransfer_initial_time
for(var/mob/new_player/N in GLOB.mob_list)
if(N.client)
N.new_player_panel_proc()
SSnightshift.check_nightshift(TRUE)
#ifdef UNIT_TESTS
// Run map tests first in case unit tests futz with map state
GLOB.test_runner.RunMap()
GLOB.test_runner.Run()
#endif
// Do this 10 second after roundstart because of roundstart lag, and make it more visible
addtimer(CALLBACK(src, PROC_REF(handle_antagfishing_reporting)), 10 SECONDS)
return TRUE
/datum/controller/subsystem/ticker/proc/station_explosion_cinematic(nuke_site = NUKE_SITE_ON_STATION, override = null)
if(cinematic)
return //already a cinematic in progress!
auto_toggle_ooc(TRUE) // Turn it on
//initialise our cinematic screen object
cinematic = new /obj/screen(src)
cinematic.icon = 'icons/effects/station_explosion.dmi'
cinematic.icon_state = "station_intact"
cinematic.layer = 21
cinematic.mouse_opacity = MOUSE_OPACITY_TRANSPARENT
cinematic.screen_loc = "1,1"
if(nuke_site == NUKE_SITE_ON_STATION)
// Kill everyone on z-level 1 except for mobs in freezers and
// malfunctioning AIs.
for(var/mob/M in GLOB.mob_list)
if(M.stat != DEAD)
var/turf/T = get_turf(M)
if(T && is_station_level(T.z) && !istype(M.loc, /obj/structure/closet/secure_closet/freezer) && !(issilicon(M) && override == "AI malfunction"))
to_chat(M, "<span class='danger'><B>The blast wave from the explosion tears you atom from atom!</B></span>")
var/mob/ghost = M.ghostize()
M.dust() //no mercy
if(ghost && ghost.client) //Play the victims an uninterrupted cinematic.
ghost.client.screen += cinematic
CHECK_TICK
if(M && M.client) //Play the survivors a cinematic.
M.client.screen += cinematic
else
for(var/mob/M in GLOB.mob_list)
if(M.client)
M.client.screen += cinematic //show every client the cinematic
switch(nuke_site)
//Now animate the cinematic
if(NUKE_SITE_ON_STATION)
// station was destroyed
if(mode && !override)
override = mode.name
switch(override)
if("nuclear emergency") //Nuke Ops successfully bombed the station
flick("intro_nuke", cinematic)
sleep(35)
flick("station_explode_fade_red", cinematic)
SEND_SOUND(world, sound('sound/effects/explosion_distant.ogg'))
cinematic.icon_state = "summary_nukewin"
if("AI malfunction") //Malf (screen,explosion,summary)
flick("intro_malf", cinematic)
sleep(76)
flick("station_explode_fade_red", cinematic)
SEND_SOUND(world, sound('sound/effects/explosion_distant.ogg'))
cinematic.icon_state = "summary_malf"
else //Station nuked (nuke,explosion,summary)
flick("intro_nuke", cinematic)
sleep(35)
flick("station_explode_fade_red", cinematic)
SEND_SOUND(world, sound('sound/effects/explosion_distant.ogg'))
cinematic.icon_state = "summary_selfdes"
if(NUKE_SITE_ON_STATION_ZLEVEL)
// nuke was nearby but (mostly) missed
if(mode && !override)
override = mode.name
switch(override)
if("nuclear emergency") //Nuke wasn't on station when it blew up
flick("intro_nuke", cinematic)
sleep(35)
SEND_SOUND(world, sound('sound/effects/explosion_distant.ogg'))
flick("station_intact_fade_red", cinematic)
cinematic.icon_state = "summary_nukefail"
if("fake") //The round isn't over, we're just freaking people out for fun
flick("intro_nuke", cinematic)
sleep(35)
SEND_SOUND(world, sound('sound/items/bikehorn.ogg'))
flick("summary_selfdes", cinematic)
else
flick("intro_nuke", cinematic)
sleep(35)
SEND_SOUND(world, sound('sound/effects/explosion_distant.ogg'))
if(NUKE_SITE_OFF_STATION_ZLEVEL, NUKE_SITE_INVALID)
// nuke was nowhere nearby
// TODO: a really distant explosion animation
sleep(50)
SEND_SOUND(world, sound('sound/effects/explosion_distant.ogg'))
//If its actually the end of the round, wait for it to end.
//Otherwise if its a verb it will continue on afterwards.
spawn(300)
QDEL_NULL(cinematic) //end the cinematic
/datum/controller/subsystem/ticker/proc/create_characters()
for(var/mob/new_player/player in GLOB.player_list)
if(player.ready && player.mind)
if(player.mind.assigned_role == "AI")
player.close_spawn_windows()
var/mob/living/silicon/ai/ai_character = player.AIize()
ai_character.moveToAILandmark()
else if(!player.mind.assigned_role)
continue
else
player.create_character()
qdel(player)
/datum/controller/subsystem/ticker/proc/equip_characters()
for(var/mob/living/carbon/human/player in GLOB.player_list)
if(player && player.mind && player.mind.assigned_role && player.mind.assigned_role != player.mind.special_role)
SSjobs.AssignRank(player, player.mind.assigned_role, FALSE)
SSjobs.EquipRank(player, player.mind.assigned_role, FALSE)
equip_cuis(player)
/datum/controller/subsystem/ticker/proc/equip_cuis(mob/living/carbon/human/H)
if(!H.client)
return // If they are spawning without a client (somehow), they *cant* have a CUI list
for(var/datum/custom_user_item/cui in H.client.cui_entries)
// Skip items with invalid character names
if((cui.characer_name != H.real_name) && !cui.all_characters_allowed)
continue
var/ok = FALSE
if(!cui.all_jobs_allowed)
var/alt_blocked = FALSE
if(H.mind.role_alt_title)
if(!(H.mind.role_alt_title in cui.allowed_jobs))
alt_blocked = TRUE
if(!(H.mind.assigned_role in cui.allowed_jobs) || alt_blocked)
continue
var/obj/item/I = new cui.object_typepath()
var/name_override = cui.item_name_override
var/desc_override = cui.item_desc_override
if(name_override)
I.name = name_override
if(desc_override)
I.desc = desc_override
if(isstorage(H.back)) // Try to place it in something on the mob's back
var/obj/item/storage/S = H.back
if(length(S.contents) < S.storage_slots)
I.forceMove(H.back)
ok = TRUE
to_chat(H, "<span class='notice'>Your [I.name] has been added to your [H.back.name].</span>")
if(!ok)
for(var/obj/item/storage/S in H.contents) // Try to place it in any item that can store stuff, on the mob.
if(length(S.contents) < S.storage_slots)
I.forceMove(S)
ok = TRUE
to_chat(H, "<span class='notice'>Your [I.name] has been added to your [S.name].</span>")
break
if(!ok) // Finally, since everything else failed, place it on the ground
var/turf/T = get_turf(H)
if(T)
I.forceMove(T)
to_chat(H, "<span class='notice'>Your [I.name] is on the [T.name] below you.</span>")
else
to_chat(H, "<span class='notice'>Your [I.name] couldnt spawn anywhere on you or even on the floor below you. Please file a bug report.</span>")
qdel(I)
/datum/controller/subsystem/ticker/proc/send_tip_of_the_round()
var/m
if(selected_tip)
m = selected_tip
else
var/list/randomtips = file2list("strings/tips.txt")
var/list/memetips = file2list("strings/sillytips.txt")
if(randomtips.len && prob(95))
m = pick(randomtips)
else if(memetips.len)
m = pick(memetips)
if(m)
to_chat(world, "<span class='purple'><b>Tip of the round: </b>[html_encode(m)]</span>")
/datum/controller/subsystem/ticker/proc/declare_completion()
GLOB.nologevent = TRUE //end of round murder and shenanigans are legal; there's no need to jam up attack logs past this point.
set_observer_default_invisibility(0) //spooks things up
//Round statistics report
var/datum/station_state/ending_station_state = new /datum/station_state()
ending_station_state.count()
var/station_integrity = min(round( 100.0 * GLOB.start_state.score(ending_station_state), 0.1), 100.0)
var/list/end_of_round_info = list()
end_of_round_info += "<BR>[TAB]Shift Duration: <B>[round(ROUND_TIME / 36000)]:[add_zero("[ROUND_TIME / 600 % 60]", 2)]:[ROUND_TIME / 100 % 6][ROUND_TIME / 100 % 10]</B>"
end_of_round_info += "<BR>[TAB]Station Integrity: <B>[mode.station_was_nuked ? "<font color='red'>Destroyed</font>" : "[station_integrity]%"]</B>"
end_of_round_info += "<BR>"
//Silicon laws report
for(var/mob/living/silicon/ai/aiPlayer in GLOB.mob_list)
var/ai_ckey = safe_get_ckey(aiPlayer)
if(aiPlayer.stat != DEAD)
end_of_round_info += "<b>[aiPlayer.name] (Played by: [ai_ckey])'s laws at the end of the game were:</b>"
else
end_of_round_info += "<b>[aiPlayer.name] (Played by: [ai_ckey])'s laws when it was deactivated were:</b>"
aiPlayer.laws_sanity_check()
for(var/datum/ai_law/law as anything in aiPlayer.laws.sorted_laws)
if(law == aiPlayer.laws.zeroth_law)
end_of_round_info += "<span class='danger'>[law.get_index()]. [law.law]</span>"
else
end_of_round_info += "[law.get_index()]. [law.law]"
if(length(aiPlayer.connected_robots))
end_of_round_info += "<b>The AI's loyal minions were:</b> "
for(var/mob/living/silicon/robot/robo in aiPlayer.connected_robots)
var/robo_ckey = safe_get_ckey(robo)
end_of_round_info += "[robo.name][robo.stat ? " (Deactivated)" : ""] (Played by: [robo_ckey])"
var/dronecount = 0
for(var/mob/living/silicon/robot/robo in GLOB.mob_list)
if(isdrone(robo))
dronecount++
continue
var/robo_ckey = safe_get_ckey(robo)
if(!robo.connected_ai)
if(robo.stat != DEAD)
end_of_round_info += "<b>[robo.name] (Played by: [robo_ckey]) survived as an AI-less borg! Its laws were:</b>"
else
end_of_round_info += "<b>[robo.name] (Played by: [robo_ckey]) was unable to survive the rigors of being a cyborg without an AI. Its laws were:</b>"
robo.laws_sanity_check()
for(var/datum/ai_law/law as anything in robo.laws.sorted_laws)
if(law == robo.laws.zeroth_law)
end_of_round_info += "<span class='danger'>[law.get_index()]. [law.law]</span>"
else
end_of_round_info += "[law.get_index()]. [law.law]"
if(dronecount)
end_of_round_info += "<b>There [dronecount > 1 ? "were" : "was"] [dronecount] industrious maintenance [dronecount > 1 ? "drones" : "drone"] this round.</b>"
if(length(mode.eventmiscs))
for(var/datum/mind/eventmind in mode.eventmiscs)
end_of_round_info += printeventplayer(eventmind)
end_of_round_info += printobjectives(eventmind)
end_of_round_info += "<br>"
mode.declare_completion()//To declare normal completion.
end_of_round_info += mode.get_end_of_round_antagonist_statistics()
for(var/datum/team/team in GLOB.antagonist_teams)
team.on_round_end()
// Save the data before end of the round griefing
SSpersistent_data.save()
to_chat(world, end_of_round_info.Join("<br>"))
// Display the scoreboard window
score.scoreboard()
// Declare the completion of the station goals
mode.declare_station_goal_completion()
//Ask the event manager to print round end information
SSevents.RoundEnd()
//make big obvious note in game logs that round ended
log_game("///////////////////////////////////////////////////////")
log_game("///////////////////// ROUND ENDED /////////////////////")
log_game("///////////////////////////////////////////////////////")
// Add AntagHUD to everyone, see who was really evil the whole time!
for(var/datum/atom_hud/antag/H in GLOB.huds)
for(var/m in GLOB.player_list)
var/mob/M = m
H.add_hud_to(M)
var/static/list/base_encouragement_messages = list(
"Keep on keeping on!",
"Great job!",
"Keep up the good work!",
"Nice going!"
)
var/static/list/special_encouragement_messages = list(
"Outstanding!",
"This is going on the fridge!",
"Looks like you're popular!",
"That's what we like to see!",
"Hell yeah, brother!",
"Honestly, quite incredible!"
)
// Tell people how many kudos they got this round
// Besides, what's another loop over the /entire player list/
var/kudos_message
for(var/mob/M in GLOB.player_list)
var/kudos = M.mind?.kudos_received_from
if(length(kudos))
kudos_message = pick(length(kudos) > 5 ? special_encouragement_messages : base_encouragement_messages)
to_chat(M, "<span class='green big'>You received <b>[length(M.mind.kudos_received_from)]</b> kudos from other players this round! [kudos_message]</span>")
// Seal the blackbox, stop collecting info
SSblackbox.Seal()
SSdbcore.SetRoundEnd()
return TRUE
/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/proc/setup_news_feeds()
var/datum/feed_channel/newChannel = new /datum/feed_channel
newChannel.channel_name = "Station Announcements Log"
newChannel.author = "Automated Announcement Listing"
newChannel.icon = "bullhorn"
newChannel.frozen = TRUE
newChannel.admin_locked = TRUE
GLOB.news_network.channels += newChannel
newChannel = new /datum/feed_channel
newChannel.channel_name = "Public Station Announcements"
newChannel.author = "Automated Announcement Listing"
newChannel.icon = "users"
newChannel.is_public = TRUE
GLOB.news_network.channels += newChannel
newChannel = new /datum/feed_channel
newChannel.channel_name = "Nyx Daily"
newChannel.author = "CentComm Minister of Information"
newChannel.icon = "meteor"
newChannel.frozen = TRUE
newChannel.admin_locked = TRUE
GLOB.news_network.channels += newChannel
newChannel = new /datum/feed_channel
newChannel.channel_name = "The Gibson Gazette"
newChannel.author = "Editor Mike Hammers"
newChannel.icon = "star"
newChannel.frozen = TRUE
newChannel.admin_locked = TRUE
GLOB.news_network.channels += newChannel
for(var/loc_type in subtypesof(/datum/trade_destination))
var/datum/trade_destination/D = new loc_type
GLOB.weighted_randomevent_locations[D] = D.viable_random_events.len
GLOB.weighted_mundaneevent_locations[D] = D.viable_mundane_events.len
// Easy handler to make rebooting the world not a massive sleep in world/Reboot()
/datum/controller/subsystem/ticker/proc/reboot_helper(reason, end_string, delay)
// Admins delayed round end. Just alert and dont bother with anything else.
if(delay_end)
to_chat(world, "<span class='boldannounceooc'>An admin has delayed the round end.</span>")
return
if(delay)
INVOKE_ASYNC(src, TYPE_PROC_REF(/datum/controller/subsystem/ticker, show_server_restart_blurb), reason)
if(!isnull(delay))
// Delay time was present. Use that.
delay = max(0, delay)
else
// Use default restart timeout
delay = restart_timeout
to_chat(world, "<span class='boldannounceooc'>Rebooting world in [delay/10] [delay > 10 ? "seconds" : "second"]. [reason]</span>")
real_reboot_time = world.time + delay
UNTIL(world.time > real_reboot_time) // Hold it here
// And if we re-delayed, bail again
if(delay_end)
to_chat(world, "<span class='boldannounceooc'>Reboot was cancelled by an admin.</span>")
return
if(end_string)
end_state = end_string
// Play a haha funny noise
var/round_end_sound = pick(GLOB.round_end_sounds)
var/sound_length = GLOB.round_end_sounds[round_end_sound]
SEND_SOUND(world, sound(round_end_sound))
sleep(sound_length)
world.Reboot()
// Timers invoke this async
/datum/controller/subsystem/ticker/proc/handle_antagfishing_reporting()
// This needs the DB
if(!SSdbcore.IsConnected())
return
// Dont need to do anything
if(!length(flagged_antag_rollers))
return
// Records themselves
var/list/datum/antag_record/records = list()
// Queries to load data (executed as async batch)
var/list/datum/db_query/load_queries = list()
// Queries to save data (executed as async batch)
var/list/datum/db_query/save_queries = list()
for(var/ckey in flagged_antag_rollers)
var/datum/antag_record/AR = new /datum/antag_record(ckey)
records[ckey] = AR
load_queries[ckey] = AR.get_load_query()
// Explanation for parameters:
// TRUE: We want warnings if these fail
// FALSE: Do NOT qdel() queries here, otherwise they wont be read. At all.
// TRUE: This is an assoc list, so it needs to prepare for that
SSdbcore.MassExecute(load_queries, TRUE, FALSE, TRUE)
// Report on things
var/list/log_text = list("The following players attempted to roll antag with no jobs (total infractions listed)")
for(var/ckey in flagged_antag_rollers)
var/datum/antag_record/AR = records[ckey]
AR.handle_data(load_queries[ckey])
save_queries[ckey] = AR.get_save_query()
log_text += "<small>- <a href='?priv_msg=[ckey]'>[ckey]</a>: [AR.infraction_count]</small>"
log_text += "Investigation advised if there are a high number of infractions"
message_admins(log_text.Join("<br>"))
// Now do a ton of saves
SSdbcore.MassExecute(save_queries, TRUE, TRUE, TRUE)
// And cleanup
QDEL_LIST_ASSOC_VAL(load_queries)
records.Cut()
flagged_antag_rollers.Cut()
/// This proc is for recording biohazard events, and blackboxing if they lived, died, or ended the round. This currently applies to: Terror spiders, Xenomorphs, and Blob.
/datum/controller/subsystem/ticker/proc/event_blackbox(outcome = ROUND_END_CREW_TRANSFER)
for(var/I in SSevents.biohazards_this_round)
switch(I)
if(TS_INFESTATION_GREEN_SPIDER, TS_INFESTATION_PRINCE_SPIDER, TS_INFESTATION_WHITE_SPIDER, TS_INFESTATION_PRINCESS_SPIDER, TS_INFESTATION_QUEEN_SPIDER)
var/output = "unknown spider type"
switch(I)
if(TS_INFESTATION_GREEN_SPIDER)
output = "Green Terrors"
if(TS_INFESTATION_PRINCE_SPIDER)
output = "Prince Terror"
if(TS_INFESTATION_WHITE_SPIDER)
output = "White Terrors"
if(TS_INFESTATION_PRINCESS_SPIDER)
output = "Princess Terrors"
if(TS_INFESTATION_QUEEN_SPIDER)
output = "Queen Terrors"
var/spiders = 0
for(var/mob/living/simple_animal/hostile/poison/terror_spider/S in GLOB.ts_spiderlist)
if(S.ckey)
spiders++
if(spiders >= 5 || (output == "Prince Terror" && spiders == 1)) //If a prince lives, record as win.
switch(outcome)
if(ROUND_END_NUCLEAR)
SSblackbox.record_feedback("tally", "Biohazard nuclear victories", 1, output)
if(ROUND_END_CREW_TRANSFER)
SSblackbox.record_feedback("tally", "Biohazard survives to normal round end", 1, output)
if(ROUND_END_FORCED)
SSblackbox.record_feedback("tally", "Biohazard survives to admin round end", 1, output)
else
switch(outcome)
if(ROUND_END_NUCLEAR)
SSblackbox.record_feedback("tally", "Biohazard dies station nuked", 1, output)
if(ROUND_END_CREW_TRANSFER)
SSblackbox.record_feedback("tally", "Biohazard dies normal end", 1, output)
if(ROUND_END_FORCED)
SSblackbox.record_feedback("tally", "Biohazard dies admin round end", 1, output)
if("Xenomorphs")
if(length(SSticker.mode.xenos) > 5)
switch(outcome)
if(ROUND_END_NUCLEAR)
SSblackbox.record_feedback("tally", "Biohazard nuclear victories", 1, "Xenomorphs")
if(ROUND_END_CREW_TRANSFER)
SSblackbox.record_feedback("tally", "Biohazard survives to normal round end", 1, "Xenomorphs")
if(ROUND_END_FORCED)
SSblackbox.record_feedback("tally", "Biohazard survives to admin round end", 1, "Xenomorphs")
else
switch(outcome)
if(ROUND_END_NUCLEAR)
SSblackbox.record_feedback("tally", "Biohazard dies station nuked", 1, "Xenomorphs")
if(ROUND_END_CREW_TRANSFER)
SSblackbox.record_feedback("tally", "Biohazard dies normal end", 1, "Xenomorphs")
if(ROUND_END_FORCED)
SSblackbox.record_feedback("tally", "Biohazard dies admin round end", 1, "Xenomorphs")
if("Blob")
if(length(SSticker.mode.blob_overminds))
switch(outcome)
if(ROUND_END_NUCLEAR)
SSblackbox.record_feedback("tally", "Biohazard nuclear victories", 1, "Blob")
if(ROUND_END_CREW_TRANSFER)
SSblackbox.record_feedback("tally", "Biohazard survives to normal round end", 1, "Blob")
if(ROUND_END_FORCED)
SSblackbox.record_feedback("tally", "Biohazard survives to admin round end", 1, "Blob")
else
switch(outcome)
if(ROUND_END_NUCLEAR)
SSblackbox.record_feedback("tally", "Biohazard dies station nuked", 1, "Blob")
if(ROUND_END_CREW_TRANSFER)
SSblackbox.record_feedback("tally", "Biohazard dies normal end", 1, "Blob")
if(ROUND_END_FORCED)
SSblackbox.record_feedback("tally", "Biohazard dies admin round end", 1, "Blob")