#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() 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(", ")].") 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(IsAdminAdvancedProcCall()) to_chat(usr, "Admin rank DB Sync blocked: Advanced ProcCall detected.") return 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 sync_ranks_with_db() var/list/sql_admins = list() for(var/i in GLOB.protected_admins) var/datum/admins/A = GLOB.protected_admins[i] var/sql_ckey = sanitizeSQL(A.target) var/sql_rank = sanitizeSQL(A.rank.name) sql_admins += list(list("ckey" = "'[sql_ckey]'", "rank" = "'[sql_rank]'")) SSdbcore.MassInsert(format_table_name("admin"), sql_admins, duplicate_key = TRUE) 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/sql_rank = sanitizeSQL(R.name) 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 = '[sql_rank]' 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 = '[sql_rank]'") if(!query_update_everything_ranks.Execute()) qdel(query_update_everything_ranks) return qdel(query_update_everything_ranks) qdel(query_check_everything_ranks)