#define POPCOUNT_SURVIVORS "survivors" //Not dead at roundend
#define POPCOUNT_ESCAPEES "escapees" //Not dead and on centcom/shuttles marked as escaped
#define POPCOUNT_SHUTTLE_ESCAPEES "shuttle_escapees" //Emergency shuttle only.
/datum/controller/subsystem/ticker/proc/gather_roundend_feedback()
var/datum/station_state/end_state = new /datum/station_state()
end_state.count()
station_integrity = min(PERCENT(GLOB.start_state.score(end_state)), 100)
gather_antag_data()
record_nuke_disk_location()
var/json_file = file("[GLOB.log_directory]/round_end_data.json")
var/list/file_data = list("escapees" = list("humans" = list(), "silicons" = list(), "others" = list(), "npcs" = list()), "abandoned" = list("humans" = list(), "silicons" = list(), "others" = list(), "npcs" = list()), "ghosts" = list(), "additional data" = list())
var/num_survivors = 0
var/num_escapees = 0
var/num_shuttle_escapees = 0
var/list/area/shuttle_areas
if(SSshuttle && SSshuttle.emergency)
shuttle_areas = SSshuttle.emergency.shuttle_areas
for(var/mob/m in GLOB.mob_list)
var/escaped
var/category
var/list/mob_data = list()
if(isnewplayer(m))
continue
if (m.client && m.client.prefs && m.client.prefs.auto_ooc)
if (!(m.client.prefs.chat_toggles & CHAT_OOC))
m.client.prefs.chat_toggles ^= CHAT_OOC
if(m.mind)
if(m.stat != DEAD && !isbrain(m) && !iscameramob(m))
num_survivors++
mob_data += list("name" = m.name, "ckey" = ckey(m.mind.key))
if(isobserver(m))
escaped = "ghosts"
else if(isliving(m))
var/mob/living/L = m
mob_data += list("location" = get_area(L), "health" = L.health)
if(ishuman(L))
var/mob/living/carbon/human/H = L
category = "humans"
mob_data += list("job" = H.mind.assigned_role, "species" = H.dna.species.name)
else if(issilicon(L))
category = "silicons"
if(isAI(L))
mob_data += list("module" = "AI")
if(isAI(L))
mob_data += list("module" = "pAI")
if(iscyborg(L))
var/mob/living/silicon/robot/R = L
mob_data += list("module" = R.module)
else
category = "others"
mob_data += list("typepath" = m.type)
if(!escaped)
if(EMERGENCY_ESCAPED_OR_ENDGAMED && (m.onCentCom() || m.onSyndieBase()))
escaped = "escapees"
num_escapees++
if(shuttle_areas[get_area(m)])
num_shuttle_escapees++
else
escaped = "abandoned"
if(!m.mind && (!ishuman(m) || !issilicon(m)))
var/list/npc_nest = file_data["[escaped]"]["npcs"]
if(npc_nest.Find(initial(m.name)))
file_data["[escaped]"]["npcs"]["[initial(m.name)]"] += 1
else
file_data["[escaped]"]["npcs"]["[initial(m.name)]"] = 1
else
if(isobserver(m))
var/pos = length(file_data["[escaped]"]) + 1
file_data["[escaped]"]["[pos]"] = mob_data
else
if(!category)
category = "others"
mob_data += list("name" = m.name, "typepath" = m.type)
var/pos = length(file_data["[escaped]"]["[category]"]) + 1
file_data["[escaped]"]["[category]"]["[pos]"] = mob_data
file_data["additional data"]["station integrity"] = station_integrity
WRITE_FILE(json_file, json_encode(file_data))
SSblackbox.record_feedback("nested tally", "round_end_stats", num_survivors, list("survivors", "total"))
SSblackbox.record_feedback("nested tally", "round_end_stats", num_escapees, list("escapees", "total"))
SSblackbox.record_feedback("nested tally", "round_end_stats", GLOB.joined_player_list.len, list("players", "total"))
SSblackbox.record_feedback("nested tally", "round_end_stats", GLOB.joined_player_list.len - num_survivors, list("players", "dead"))
. = list()
.[POPCOUNT_SURVIVORS] = num_survivors
.[POPCOUNT_ESCAPEES] = num_escapees
.[POPCOUNT_SHUTTLE_ESCAPEES] = num_shuttle_escapees
.["station_integrity"] = station_integrity
/datum/controller/subsystem/ticker/proc/gather_antag_data()
var/team_gid = 1
var/list/team_ids = list()
for(var/datum/antagonist/A in GLOB.antagonists)
if(!A.owner)
continue
var/list/antag_info = list()
antag_info["key"] = A.owner.key
antag_info["name"] = A.owner.name
antag_info["antagonist_type"] = A.type
antag_info["antagonist_name"] = A.name //For auto and custom roles
antag_info["objectives"] = list()
antag_info["team"] = list()
var/datum/team/T = A.get_team()
if(T)
antag_info["team"]["type"] = T.type
antag_info["team"]["name"] = T.name
if(!team_ids[T])
team_ids[T] = team_gid++
antag_info["team"]["id"] = team_ids[T]
if(A.objectives.len)
for(var/datum/objective/O in A.objectives)
var/result = "UNKNOWN"
var/actual_result = O.check_completion()
if(actual_result >= 1)
result = "SUCCESS"
else if(actual_result <= 0)
result = "FAIL"
else
result = "[actual_result*100]%"
antag_info["objectives"] += list(list("objective_type"=O.type,"text"=O.explanation_text,"result"=result))
SSblackbox.record_feedback("associative", "antagonists", 1, antag_info)
/datum/controller/subsystem/ticker/proc/record_nuke_disk_location()
var/obj/item/disk/nuclear/N = locate() in GLOB.poi_list
if(N)
var/list/data = list()
var/turf/T = get_turf(N)
if(T)
data["x"] = T.x
data["y"] = T.y
data["z"] = T.z
var/atom/outer = get_atom_on_turf(N,/mob/living)
if(outer != N)
if(isliving(outer))
var/mob/living/L = outer
data["holder"] = L.real_name
else
data["holder"] = outer.name
SSblackbox.record_feedback("associative", "roundend_nukedisk", 1 , data)
/datum/controller/subsystem/ticker/proc/gather_newscaster()
var/json_file = file("[GLOB.log_directory]/newscaster.json")
var/list/file_data = list()
var/pos = 1
for(var/V in GLOB.news_network.network_channels)
var/datum/news/feed_channel/channel = V
if(!istype(channel))
stack_trace("Non-channel in newscaster channel list")
continue
file_data["[pos]"] = list("channel name" = "[channel.channel_name]", "author" = "[channel.author]", "censored" = channel.censored ? 1 : 0, "author censored" = channel.authorCensor ? 1 : 0, "messages" = list())
for(var/M in channel.messages)
var/datum/news/feed_message/message = M
if(!istype(message))
stack_trace("Non-message in newscaster channel messages list")
continue
var/list/comment_data = list()
for(var/C in message.comments)
var/datum/news/feed_comment/comment = C
if(!istype(comment))
stack_trace("Non-message in newscaster message comments list")
continue
comment_data += list(list("author" = "[comment.author]", "time stamp" = "[comment.time_stamp]", "body" = "[comment.body]"))
file_data["[pos]"]["messages"] += list(list("author" = "[message.author]", "time stamp" = "[message.time_stamp]", "censored" = message.bodyCensor ? 1 : 0, "author censored" = message.authorCensor ? 1 : 0, "photo file" = "[message.photo_file]", "photo caption" = "[message.caption]", "body" = "[message.body]", "comments" = comment_data))
pos++
if(GLOB.news_network.wanted_issue.active)
file_data["wanted"] = list("author" = "[GLOB.news_network.wanted_issue.scannedUser]", "criminal" = "[GLOB.news_network.wanted_issue.criminal]", "description" = "[GLOB.news_network.wanted_issue.body]", "photo file" = "[GLOB.news_network.wanted_issue.photo_file]")
WRITE_FILE(json_file, json_encode(file_data))
/datum/controller/subsystem/ticker/proc/declare_completion()
set waitfor = FALSE
to_chat(world, "
The round has ended.")
if(LAZYLEN(GLOB.round_end_notifiees))
world.TgsTargetedChatBroadcast("[GLOB.round_end_notifiees.Join(", ")] the round has ended.", FALSE)
for(var/I in round_end_events)
var/datum/callback/cb = I
cb.InvokeAsync()
LAZYCLEARLIST(round_end_events)
for(var/client/C in GLOB.clients)
if(!C.credits)
C.RollCredits()
C.playtitlemusic(40)
CONFIG_SET(flag/suicide_allowed,TRUE) // EORG suicides allowed
var/popcount = gather_roundend_feedback()
display_report(popcount)
CHECK_TICK
// 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)
CHECK_TICK
//Set news report and mode result
mode.set_round_result()
var/survival_rate = GLOB.joined_player_list.len ? "[PERCENT(popcount[POPCOUNT_SURVIVORS]/GLOB.joined_player_list.len)]%" : "there's literally no player"
send2irc("Server", "A round of [mode.name] just ended[mode_result == "undefined" ? "." : " with a [mode_result]."] Survival rate: [survival_rate]")
if(length(CONFIG_GET(keyed_list/cross_server)))
send_news_report()
//tell the nice people on discord what went on before the salt cannon happens.
world.TgsTargetedChatBroadcast("The current round has ended. Please standby for your shift interlude Nanotrasen News Network's report!", FALSE)
world.TgsTargetedChatBroadcast(send_news_report(),FALSE)
CHECK_TICK
//These need update to actually reflect the real antagonists
//Print a list of antagonists to the server log
var/list/total_antagonists = list()
//Look into all mobs in world, dead or alive
for(var/datum/antagonist/A in GLOB.antagonists)
if(!A.owner)
continue
if(!(A.name in total_antagonists))
total_antagonists[A.name] = list()
total_antagonists[A.name] += "[key_name(A.owner)]"
CHECK_TICK
//Now print them all into the log!
log_game("Antagonists at round end were...")
for(var/antag_name in total_antagonists)
var/list/L = total_antagonists[antag_name]
log_game("[antag_name]s :[L.Join(", ")].")
set_observer_default_invisibility(0, "The round is over! You are now visible to the living.")
CHECK_TICK
SSdbcore.SetRoundEnd()
//Collects persistence features
if(mode.allow_persistence_save)
SSpersistence.CollectData()
//stop collecting feedback during grifftime
SSblackbox.Seal()
sleep(50)
ready_for_reboot = TRUE
standard_reboot()
/datum/controller/subsystem/ticker/proc/standard_reboot()
if(ready_for_reboot)
if(mode.station_was_nuked)
Reboot("Station destroyed by Nuclear Device.", "nuke")
else
Reboot("Round ended.", "proper completion")
else
CRASH("Attempted standard reboot without ticker roundend completion")
//Common part of the report
/datum/controller/subsystem/ticker/proc/build_roundend_report()
var/list/parts = list()
//Gamemode specific things. Should be empty most of the time.
parts += mode.special_report()
CHECK_TICK
//AI laws
parts += law_report()
CHECK_TICK
//Antagonists
parts += antag_report()
CHECK_TICK
//Medals
parts += medal_report()
//Station Goals
parts += goal_report()
listclearnulls(parts)
return parts.Join()
/datum/controller/subsystem/ticker/proc/survivor_report(popcount)
var/list/parts = list()
var/station_evacuated = EMERGENCY_ESCAPED_OR_ENDGAMED
if(GLOB.round_id)
var/statspage = CONFIG_GET(string/roundstatsurl)
var/info = statspage ? "[GLOB.round_id]" : GLOB.round_id
parts += "[FOURSPACES]Round ID: [info]"
var/list/voting_results = SSvote.stored_gamemode_votes
if(length(voting_results))
parts += "[FOURSPACES]Voting: "
var/total_score = 0
for(var/choice in voting_results)
var/score = voting_results[choice]
total_score += score
parts += "[FOURSPACES][FOURSPACES][choice]: [score]"
parts += "[FOURSPACES]Shift Duration: [DisplayTimeText(world.time - SSticker.round_start_time)]"
parts += "[FOURSPACES]Station Integrity: [mode.station_was_nuked ? "Destroyed" : "[popcount["station_integrity"]]%"]"
var/total_players = GLOB.joined_player_list.len
if(total_players)
parts+= "[FOURSPACES]Total Population: [total_players]"
if(station_evacuated)
parts += "
[FOURSPACES]Evacuation Rate: [popcount[POPCOUNT_ESCAPEES]] ([PERCENT(popcount[POPCOUNT_ESCAPEES]/total_players)]%)"
parts += "[FOURSPACES](on emergency shuttle): [popcount[POPCOUNT_SHUTTLE_ESCAPEES]] ([PERCENT(popcount[POPCOUNT_SHUTTLE_ESCAPEES]/total_players)]%)"
parts += "[FOURSPACES]Survival Rate: [popcount[POPCOUNT_SURVIVORS]] ([PERCENT(popcount[POPCOUNT_SURVIVORS]/total_players)]%)"
if(SSblackbox.first_death)
var/list/ded = SSblackbox.first_death
if(ded.len)
parts += "[FOURSPACES]First Death: [ded["name"]], [ded["role"]], at [ded["area"]]. Damage taken: [ded["damage"]].[ded["last_words"] ? " Their last words were: \"[ded["last_words"]]\"" : ""]"
//ignore this comment, it fixes the broken sytax parsing caused by the " above
else
parts += "[FOURSPACES]Nobody died this shift!"
if(istype(SSticker.mode, /datum/game_mode/dynamic))
var/datum/game_mode/dynamic/mode = SSticker.mode
mode.update_playercounts()
parts += "[FOURSPACES]Final threat level: [mode.threat_level]"
parts += "[FOURSPACES]Final threat: [mode.threat]"
parts += "[FOURSPACES]Average threat: [mode.threat_average]"
parts += "[FOURSPACES]Executed rules:"
for(var/datum/dynamic_ruleset/rule in mode.executed_rules)
parts += "[FOURSPACES][FOURSPACES][rule.ruletype] - [rule.name]: -[rule.cost + rule.scaled_times * rule.scaling_cost] threat"
parts += "[FOURSPACES]Other threat changes:"
for(var/str in mode.threat_log)
parts += "[FOURSPACES][FOURSPACES][str]"
for(var/entry in mode.threat_tallies)
parts += "[FOURSPACES][FOURSPACES][entry] added [mode.threat_tallies[entry]]"
SSblackbox.record_feedback("tally","dynamic_threat",mode.threat_level,"Final threat level")
SSblackbox.record_feedback("tally","dynamic_threat",mode.threat,"Final Threat")
return parts.Join("
")
/client/proc/roundend_report_file()
return "data/roundend_reports/[ckey].html"
/datum/controller/subsystem/ticker/proc/show_roundend_report(client/C, previous = FALSE)
var/datum/browser/roundend_report = new(C, "roundend")
roundend_report.width = 800
roundend_report.height = 600
var/content
var/filename = C.roundend_report_file()
if(!previous)
var/list/report_parts = list(personal_report(C), GLOB.common_report)
content = report_parts.Join()
C.verbs -= /client/proc/show_previous_roundend_report
fdel(filename)
text2file(content, filename)
else
content = file2text(filename)
roundend_report.set_content(content)
roundend_report.stylesheets = list()
roundend_report.add_stylesheet("roundend", 'html/browser/roundend.css')
roundend_report.add_stylesheet("font-awesome", 'html/font-awesome/css/all.min.css')
roundend_report.open(0)
/datum/controller/subsystem/ticker/proc/personal_report(client/C, popcount)
var/list/parts = list()
var/mob/M = C.mob
if(M.mind && !isnewplayer(M))
if(M.stat != DEAD && !isbrain(M))
if(EMERGENCY_ESCAPED_OR_ENDGAMED)
if(!M.onCentCom() && !M.onSyndieBase())
parts += "