#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() 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.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 var/datum/station_state/end_state = new /datum/station_state() end_state.count() var/station_integrity = min(PERCENT(GLOB.start_state.score(end_state)), 100) 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 = O.check_completion() ? "SUCCESS" : "FAIL" 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/newscaster/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/newscaster/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/newscaster/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)) send2irc("Notice", "[GLOB.round_end_notifiees.Join(", ")] the round has ended.") 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) 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() send2irc("Server", "Round just ended.") if(length(CONFIG_GET(keyed_list/cross_server))) send_news_report() 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(", ")].") 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 += "[GLOB.TAB]Round ID: [info]" parts += "[GLOB.TAB]Shift Duration: [DisplayTimeText(world.time - SSticker.round_start_time)]" parts += "[GLOB.TAB]Station Integrity: [mode.station_was_nuked ? "Destroyed" : "[popcount["station_integrity"]]%"]" var/total_players = GLOB.joined_player_list.len if(total_players) parts+= "[GLOB.TAB]Total Population: [total_players]" if(station_evacuated) parts += "
[GLOB.TAB]Evacuation Rate: [popcount[POPCOUNT_ESCAPEES]] ([PERCENT(popcount[POPCOUNT_ESCAPEES]/total_players)]%)" parts += "[GLOB.TAB](on emergency shuttle): [popcount[POPCOUNT_SHUTTLE_ESCAPEES]] ([PERCENT(popcount[POPCOUNT_SHUTTLE_ESCAPEES]/total_players)]%)" parts += "[GLOB.TAB]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 += "[GLOB.TAB]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 += "[GLOB.TAB]Nobody died this shift!" 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.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 += "
" parts += "You managed to survive, but were marooned on [station_name()]..." else parts += "
" parts += "You managed to survive the events on [station_name()] as [M.real_name]." else parts += "
" parts += "You managed to survive the events on [station_name()] as [M.real_name]." else parts += "
" parts += "You did not survive the events on [station_name()]..." else parts += "
" parts += "
" parts += GLOB.survivor_report parts += "
" return parts.Join() /datum/controller/subsystem/ticker/proc/display_report(popcount) GLOB.common_report = build_roundend_report() GLOB.survivor_report = survivor_report(popcount) for(var/client/C in GLOB.clients) show_roundend_report(C, FALSE) give_show_report_button(C) CHECK_TICK /datum/controller/subsystem/ticker/proc/law_report() var/list/parts = list() var/borg_spacer = FALSE //inserts an extra linebreak to seperate AIs from independent borgs, and then multiple independent borgs. //Silicon laws report for (var/i in GLOB.ai_list) var/mob/living/silicon/ai/aiPlayer = i if(aiPlayer.mind) parts += "[aiPlayer.name] (Played by: [aiPlayer.mind.key])'s laws [aiPlayer.stat != DEAD ? "at the end of the round" : "when it was deactivated"] were:" parts += aiPlayer.laws.get_law_list(include_zeroth=TRUE) parts += "Total law changes: [aiPlayer.law_change_counter]" if (aiPlayer.connected_robots.len) var/borg_num = aiPlayer.connected_robots.len var/robolist = "
[aiPlayer.real_name]'s minions were: " for(var/mob/living/silicon/robot/robo in aiPlayer.connected_robots) borg_num-- if(robo.mind) robolist += "[robo.name] (Played by: [robo.mind.key])[robo.stat == DEAD ? " (Deactivated)" : ""][borg_num ?", ":""]
" parts += "[robolist]" if(!borg_spacer) borg_spacer = TRUE for (var/mob/living/silicon/robot/robo in GLOB.silicon_mobs) if (!robo.connected_ai && robo.mind) parts += "[borg_spacer?"
":""][robo.name] (Played by: [robo.mind.key]) [(robo.stat != DEAD)? "survived as an AI-less borg!" : "was unable to survive the rigors of being a cyborg without an AI."] Its laws were:" if(robo) //How the hell do we lose robo between here and the world messages directly above this? parts += robo.laws.get_law_list(include_zeroth=TRUE) if(!borg_spacer) borg_spacer = TRUE if(parts.len) return "
[parts.Join("
")]
" else return "" /datum/controller/subsystem/ticker/proc/goal_report() var/list/parts = list() if(mode.station_goals.len) for(var/V in mode.station_goals) var/datum/station_goal/G = V parts += G.get_result() return "
    [parts.Join()]
" /datum/controller/subsystem/ticker/proc/medal_report() if(GLOB.commendations.len) var/list/parts = list() parts += "Medal Commendations:" for (var/com in GLOB.commendations) parts += com return "
[parts.Join("
")]
" return "" /datum/controller/subsystem/ticker/proc/antag_report() var/list/result = list() var/list/all_teams = list() var/list/all_antagonists = list() for(var/datum/antagonist/A in GLOB.antagonists) if(!A.owner) continue all_teams |= A.get_team() all_antagonists += A for(var/datum/team/T in all_teams) result += T.roundend_report() for(var/datum/antagonist/X in all_antagonists) if(X.get_team() == T) all_antagonists -= X result += " "//newline between teams CHECK_TICK var/currrent_category var/datum/antagonist/previous_category sortTim(all_antagonists, /proc/cmp_antag_category) for(var/datum/antagonist/A in all_antagonists) if(!A.show_in_roundend) continue if(A.roundend_category != currrent_category) if(previous_category) result += previous_category.roundend_report_footer() result += "
" result += "
" result += A.roundend_report_header() currrent_category = A.roundend_category previous_category = A result += A.roundend_report() result += "

" CHECK_TICK if(all_antagonists.len) var/datum/antagonist/last = all_antagonists[all_antagonists.len] result += last.roundend_report_footer() result += "
" return result.Join() /proc/cmp_antag_category(datum/antagonist/A,datum/antagonist/B) return sorttext(B.roundend_category,A.roundend_category) /datum/controller/subsystem/ticker/proc/give_show_report_button(client/C) var/datum/action/report/R = new C.player_details.player_actions += R R.Grant(C.mob) to_chat(C,"Show roundend report again") /datum/action/report name = "Show roundend report" button_icon_state = "round_end" /datum/action/report/Trigger() if(owner && GLOB.common_report && SSticker.current_state == GAME_STATE_FINISHED) SSticker.show_roundend_report(owner.client, FALSE) /datum/action/report/IsAvailable() return 1 /datum/action/report/Topic(href,href_list) if(usr != owner) return if(href_list["report"]) Trigger() return /proc/printplayer(datum/mind/ply, fleecheck) var/jobtext = "" if(ply.assigned_role) jobtext = " the [ply.assigned_role]" var/text = "[ply.key] was [ply.name][jobtext] and" if(ply.current) if(ply.current.stat == DEAD) text += " died" else text += " survived" if(fleecheck) var/turf/T = get_turf(ply.current) if(!T || !is_station_level(T.z)) text += " while fleeing the station" if(ply.current.real_name != ply.name) text += " as [ply.current.real_name]" else text += " had their body destroyed" return text /proc/printplayerlist(list/players,fleecheck) var/list/parts = list() parts += "
    " for(var/datum/mind/M in players) parts += "
  • [printplayer(M,fleecheck)]
  • " parts += "
" return parts.Join() /proc/printobjectives(datum/mind/ply) var/list/objective_parts = list() var/count = 1 for(var/datum/objective/objective in ply.objectives) if(objective.check_completion()) objective_parts += "Objective #[count]: [objective.explanation_text] Success!" else objective_parts += "Objective #[count]: [objective.explanation_text] Fail." count++ return objective_parts.Join("
") /datum/controller/subsystem/ticker/proc/save_admin_data() if(CONFIG_GET(flag/admin_legacy_system)) //we're already using legacy system so there's nothing to save return else if(load_admins(TRUE)) //returns true if there was a database failure and the backup was loaded from return var/datum/DBQuery/query_admin_rank_update = SSdbcore.NewQuery("UPDATE [format_table_name("player")] p INNER JOIN [format_table_name("admin")] a ON p.ckey = a.ckey SET p.lastadminrank = a.rank") query_admin_rank_update.Execute() qdel(query_admin_rank_update) //json format backup file generation stored per server var/json_file = file("data/admins_backup.json") var/list/file_data = list("ranks" = list(), "admins" = list()) for(var/datum/admin_rank/R in GLOB.admin_ranks) file_data["ranks"]["[R.name]"] = list() file_data["ranks"]["[R.name]"]["include rights"] = R.include_rights file_data["ranks"]["[R.name]"]["exclude rights"] = R.exclude_rights file_data["ranks"]["[R.name]"]["can edit rights"] = R.can_edit_rights for(var/i in GLOB.admin_datums+GLOB.deadmins) var/datum/admins/A = GLOB.admin_datums[i] if(!A) A = GLOB.deadmins[i] if (!A) continue file_data["admins"]["[i]"] = A.rank.name fdel(json_file) WRITE_FILE(json_file, json_encode(file_data)) /datum/controller/subsystem/ticker/proc/update_everything_flag_in_db() for(var/datum/admin_rank/R in GLOB.admin_ranks) var/list/flags = list() if(R.include_rights == R_EVERYTHING) flags += "flags" if(R.exclude_rights == R_EVERYTHING) flags += "exclude_flags" if(R.can_edit_rights == R_EVERYTHING) flags += "can_edit_flags" if(!flags.len) continue var/flags_to_check = flags.Join(" != [R_EVERYTHING] AND ") + " != [R_EVERYTHING]" var/datum/DBQuery/query_check_everything_ranks = SSdbcore.NewQuery("SELECT flags, exclude_flags, can_edit_flags FROM [format_table_name("admin_ranks")] WHERE rank = '[R.name]' AND ([flags_to_check])") if(!query_check_everything_ranks.Execute()) qdel(query_check_everything_ranks) return if(query_check_everything_ranks.NextRow()) //no row is returned if the rank already has the correct flag value var/flags_to_update = flags.Join(" = [R_EVERYTHING], ") + " = [R_EVERYTHING]" var/datum/DBQuery/query_update_everything_ranks = SSdbcore.NewQuery("UPDATE [format_table_name("admin_ranks")] SET [flags_to_update] WHERE rank = '[R.name]'") if(!query_update_everything_ranks.Execute()) qdel(query_update_everything_ranks) return qdel(query_update_everything_ranks) qdel(query_check_everything_ranks)