#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. #define PERSONAL_LAST_ROUND "personal last round" #define SERVER_LAST_ROUND "server last round" /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") // All but npcs sublists and ghost category contain only mobs with minds 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 //Count of non-brain non-camera mobs with mind that are alive var/num_escapees = 0 //Above and on centcom z var/num_shuttle_escapees = 0 //Above and on escape shuttle var/list/area/shuttle_areas if(SSshuttle?.emergency) shuttle_areas = SSshuttle.emergency.shuttle_areas for(var/mob/M in GLOB.mob_list) var/list/mob_data = list() if(isnewplayer(M)) continue // enable their ooc? if (M.client?.prefs?.auto_ooc) if (!(M.client.prefs.chat_toggles & CHAT_OOC)) M.client.prefs.chat_toggles ^= CHAT_OOC var/escape_status = "abandoned" //default to abandoned var/category = "npcs" //Default to simple count only bracket var/count_only = TRUE //Count by name only or full info mob_data["name"] = M.name if(M.mind) count_only = FALSE mob_data["ckey"] = M.mind.key if(M.stat != DEAD && !isbrain(M) && !iscameramob(M)) num_survivors++ if(EMERGENCY_ESCAPED_OR_ENDGAMED && (M.onCentCom() || M.onSyndieBase())) num_escapees++ escape_status = "escapees" if(shuttle_areas[get_area(M)]) num_shuttle_escapees++ if(isliving(M)) var/mob/living/L = M mob_data["location"] = get_area(L) mob_data["health"] = L.health if(ishuman(L)) var/mob/living/carbon/human/H = L category = "humans" if(H.mind) mob_data["job"] = H.mind.assigned_role else mob_data["job"] = "Unknown" mob_data["species"] = H.dna.species.name else if(issilicon(L)) category = "silicons" if(isAI(L)) mob_data["module"] = "AI" else if(ispAI(L)) mob_data["module"] = "pAI" else if(iscyborg(L)) var/mob/living/silicon/robot/R = L mob_data["module"] = R.module.name else category = "others" mob_data["typepath"] = M.type //Ghosts don't care about minds, but we want to retain ckey data etc if(isobserver(M)) count_only = FALSE escape_status = "ghosts" if(!M.mind) mob_data["ckey"] = M.key category = null //ghosts are one list deep //All other mindless stuff just gets counts by name if(count_only) var/list/npc_nest = file_data["[escape_status]"]["npcs"] var/name_to_use = initial(M.name) if(ishuman(M)) name_to_use = "Unknown Human" //Monkeymen and other mindless corpses if(npc_nest.Find(name_to_use)) file_data["[escape_status]"]["npcs"][name_to_use] += 1 else file_data["[escape_status]"]["npcs"][name_to_use] = 1 else //Mobs with minds and ghosts get detailed data if(category) var/pos = length(file_data["[escape_status]"]["[category]"]) + 1 file_data["[escape_status]"]["[category]"]["[pos]"] = mob_data else var/pos = length(file_data["[escape_status]"]) + 1 file_data["[escape_status]"]["[pos]"] = mob_data var/datum/station_state/end_state = new /datum/station_state() end_state.count() 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 = "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)) ///Handles random hardcore point rewarding if it applies. /datum/controller/subsystem/ticker/proc/HandleRandomHardcoreScore(client/player_client) if(!ishuman(player_client.mob)) return FALSE var/mob/living/carbon/human/human_mob = player_client.mob if(!human_mob.hardcore_survival_score) ///no score no glory return FALSE if(human_mob.mind && (human_mob.mind.special_role || length(human_mob.mind.antag_datums) > 0)) var/didthegamerwin = TRUE for(var/a in human_mob.mind.antag_datums) var/datum/antagonist/antag_datum = a for(var/i in antag_datum.objectives) var/datum/objective/objective_datum = i if(!objective_datum.check_completion()) didthegamerwin = FALSE if(!didthegamerwin) return FALSE player_client.give_award(/datum/award/score/hardcore_random, human_mob, round(human_mob.hardcore_survival_score)) else if(human_mob.onCentCom()) player_client.give_award(/datum/award/score/hardcore_random, human_mob, round(human_mob.hardcore_survival_score)) /datum/controller/subsystem/ticker/proc/declare_completion() set waitfor = FALSE to_chat(world, "


The round has ended.") log_game("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/speed_round = FALSE if(world.time - SSticker.round_start_time <= 300 SECONDS) speed_round = TRUE for(var/client/C in GLOB.clients) if(!C.credits) C.RollCredits() C.playtitlemusic(40) if(speed_round) C.give_award(/datum/award/achievement/misc/speed_round, C.mob) HandleRandomHardcoreScore(C) 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" send2adminchat("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 // handle_hearts() set_observer_default_invisibility(0, "The round is over! You are now visible to the living.") 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.SaveTCGCards() 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() parts += hardcore_random_report() CHECK_TICK //Medals parts += medal_report() //Station Goals parts += goal_report() //Economy & Money parts += market_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!" var/avg_threat = SSactivity.get_average_threat() var/max_threat = SSactivity.get_max_threat() parts += "[FOURSPACES]Threat at round end: [SSactivity.current_threat]" parts += "[FOURSPACES]Average threat: [avg_threat]" parts += "[FOURSPACES]Max threat: [max_threat]" if(istype(SSticker.mode, /datum/game_mode/dynamic)) var/datum/game_mode/dynamic/mode = SSticker.mode mode.update_playercounts() // ? parts += "[FOURSPACES]Target threat: [mode.threat_level]" 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","threat",mode.threat_level,"Target threat") SSblackbox.record_feedback("tally","threat",SSactivity.current_threat,"Final Threat") SSblackbox.record_feedback("tally","threat",avg_threat,"Average Threat") SSblackbox.record_feedback("tally","threat",max_threat,"Max Threat") return parts.Join("
") /client/proc/roundend_report_file() return "data/roundend_reports/[ckey].html" /** * Log the round-end report as an HTML file * * Composits the roundend report, and saves it in two locations. * The report is first saved along with the round's logs * Then, the report is copied to a fixed directory specifically for * housing the server's last roundend report. In this location, * the file will be overwritten at the end of each shift. */ /datum/controller/subsystem/ticker/proc/log_roundend_report() var/roundend_file = file("[GLOB.log_directory]/round_end_data.html") var/list/parts = list() parts += "
" parts += GLOB.survivor_report parts += "
" parts += GLOB.common_report var/content = parts.Join() //Log the rendered HTML in the round log directory fdel(roundend_file) WRITE_FILE(roundend_file, content) //Place a copy in the root folder, to be overwritten each round. roundend_file = file("data/server_last_roundend_report.html") fdel(roundend_file) WRITE_FILE(roundend_file, content) /datum/controller/subsystem/ticker/proc/show_roundend_report(client/C, report_type = null) 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(report_type == PERSONAL_LAST_ROUND) //Look at this player's last round content = file2text(filename) else if (report_type == SERVER_LAST_ROUND) //Look at the last round that this server has seen content = file2text("data/server_last_roundend_report.html") else //report_type is null, so make a new report based on the current round and show that to the player var/list/report_parts = list(personal_report(C), GLOB.common_report) content = report_parts.Join() fdel(filename) text2file(content, 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(FALSE) /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) log_roundend_report() for(var/client/C in GLOB.clients) show_roundend_report(C) 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][aiPlayer.mind.hide_ckey ? "" : " (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 parts += "
[aiPlayer.real_name]'s minions were:" for(var/mob/living/silicon/robot/robo in aiPlayer.connected_robots) borg_num-- if(robo.mind) parts += "[robo.name][robo.mind.hide_ckey ? "" : " (Played by: [robo.mind.key])"] [robo.stat == DEAD ? " (Deactivated)" : ""][borg_num ?", ":""]" 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][robo.mind.hide_ckey ? "" : " (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()]
" ///Generate a report for how much money is on station, as well as the richest crewmember on the station. /datum/controller/subsystem/ticker/proc/market_report() var/list/parts = list() parts += "Station Economic Summary:" ///This is the richest account on station at roundend. var/datum/bank_account/mr_moneybags ///This is the station's total wealth at the end of the round. var/station_vault = 0 ///How many players joined the round. var/total_players = GLOB.joined_player_list.len var/list/typecache_bank = typecacheof(list(/datum/bank_account/department, /datum/bank_account/remote)) for(var/datum/bank_account/current_acc in SSeconomy.generated_accounts) if(typecache_bank[current_acc.type]) continue station_vault += current_acc.account_balance if(!mr_moneybags || mr_moneybags.account_balance < current_acc.account_balance) mr_moneybags = current_acc parts += "
There were [station_vault] credits collected by crew this shift.
" if(total_players > 0) parts += "An average of [station_vault/total_players] credits were collected.
" // log_econ("Roundend credit total: [station_vault] credits. Average Credits: [station_vault/total_players]") if(mr_moneybags) parts += "The most affluent crew member at shift end was [mr_moneybags.account_holder] with [mr_moneybags.account_balance] cr!
" else parts += "Somehow, nobody made any money this shift! This'll result in some budget cuts...
" return parts /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 "" ///Generate a report for all players who made it out alive with a hardcore random character and prints their final score /datum/controller/subsystem/ticker/proc/hardcore_random_report() . = list() var/list/hardcores = list() for(var/i in GLOB.player_list) if(!ishuman(i)) continue var/mob/living/carbon/human/human_player = i if(!human_player.hardcore_survival_score || !human_player.onCentCom() || human_player.stat == DEAD) ///gotta escape nerd continue if(!human_player.mind) continue hardcores += human_player if(!length(hardcores)) return . += "
The following people made it out as a random hardcore character:" . += "
    " for(var/mob/living/carbon/human/human_player in hardcores) . += "
  • [printplayer(human_player.mind)] with a hardcore random score of [round(human_player.hardcore_survival_score)]
  • " . += "
" /datum/controller/subsystem/ticker/proc/antag_report() var/list/result = list() var/list/all_teams = list() var/list/all_antagonists = list() // for(var/datum/team/A in GLOB.antagonist_teams) // all_teams |= A 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) /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.hide_ckey ? \ "[ply.key] was [ply.name][jobtext] and" \ : "[ply.name][jobtext]") 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 += "" return parts.Join() /proc/printobjectives(list/objectives) if(!objectives || !objectives.len) return var/list/objective_parts = list() var/count = 1 for(var/datum/objective/objective in objectives) if(objective.completable) var/completion = objective.check_completion() if(completion >= 1) objective_parts += "Objective #[count]: [objective.explanation_text] Success!" else if(completion <= 0) objective_parts += "Objective #[count]: [objective.explanation_text] Fail." else objective_parts += "Objective #[count]: [objective.explanation_text] [completion*100]%" else objective_parts += "Objective #[count]: [objective.explanation_text]" 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] sql_admins += list(list("ckey" = A.target, "rank" = A.rank.name)) SSdbcore.MassInsert(format_table_name("admin"), sql_admins, duplicate_key = TRUE) var/datum/db_query/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/db_query/query_check_everything_ranks = SSdbcore.NewQuery( "SELECT flags, exclude_flags, can_edit_flags FROM [format_table_name("admin_ranks")] WHERE rank = :rank AND ([flags_to_check])", list("rank" = R.name) ) 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/db_query/query_update_everything_ranks = SSdbcore.NewQuery( "UPDATE [format_table_name("admin_ranks")] SET [flags_to_update] WHERE rank = :rank", list("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)