diff --git a/code/__HELPERS/roundend.dm b/code/__HELPERS/roundend.dm
new file mode 100644
index 0000000000..c8710d6570
--- /dev/null
+++ b/code/__HELPERS/roundend.dm
@@ -0,0 +1,432 @@
+/datum/controller/subsystem/ticker/proc/gather_roundend_feedback()
+ var/clients = GLOB.player_list.len
+ var/surviving_humans = 0
+ var/surviving_total = 0
+ var/ghosts = 0
+ var/escaped_humans = 0
+ var/escaped_total = 0
+
+ for(var/mob/M in GLOB.player_list)
+ if(ishuman(M))
+ if(!M.stat)
+ surviving_humans++
+ if(M.z == ZLEVEL_CENTCOM)
+ escaped_humans++
+ if(!M.stat)
+ surviving_total++
+ if(M.z == ZLEVEL_CENTCOM)
+ escaped_total++
+
+ if(isobserver(M))
+ ghosts++
+
+ if(clients)
+ SSblackbox.record_feedback("nested tally", "round_end_stats", clients, list("clients"))
+ if(ghosts)
+ SSblackbox.record_feedback("nested tally", "round_end_stats", ghosts, list("ghosts"))
+ if(surviving_humans)
+ SSblackbox.record_feedback("nested tally", "round_end_stats", surviving_humans, list("survivors", "human"))
+ if(surviving_total)
+ SSblackbox.record_feedback("nested tally", "round_end_stats", surviving_total, list("survivors", "total"))
+ if(escaped_humans)
+ SSblackbox.record_feedback("nested tally", "round_end_stats", escaped_humans, list("escapees", "human"))
+ if(escaped_total)
+ SSblackbox.record_feedback("nested tally", "round_end_stats", escaped_total, list("escapees", "total"))
+
+ gather_antag_success_rate()
+
+/datum/controller/subsystem/ticker/proc/gather_antag_success_rate()
+ var/team_gid = 1
+ var/list/team_ids = list()
+
+ for(var/datum/antagonist/A in GLOB.antagonists)
+ 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/objective_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.owner)
+ continue
+ 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/gather_newscaster()
+ var/json_file = file("[GLOB.log_directory]/newscaster.json")
+ var/list/file_data = list()
+ var/pos = 1
+ for(var/datum/newscaster/feed_channel/channel in GLOB.news_network.network_channels)
+ if(!GLOB.news_network.network_channels.len)
+ break
+ 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())
+ if(!channel.messages.len)
+ continue
+ for(var/datum/newscaster/feed_message/message in channel.messages)
+ file_data["[pos]"]["messages"] |= 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" = list())
+ if(!message.comments.len)
+ continue
+ for(var/datum/newscaster/feed_comment/comment in message.comments)
+ file_data["[pos]"]["messages"]["comments"] = list("author" = "[comment.author]", "time stamp" = "[comment.time_stamp]", "body" = "[comment.body]")
+ 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/client/C in GLOB.clients)
+ if(!C.credits)
+ C.RollCredits()
+ C.playtitlemusic(40)
+
+ display_report()
+
+ gather_roundend_feedback()
+
+ CHECK_TICK
+
+ //Set news report and mode result
+ mode.set_round_result()
+
+ send2irc("Server", "Round just ended.")
+
+ if(length(CONFIG_GET(keyed_string_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/mind/Mind in minds)
+ var/temprole = Mind.special_role
+ if(temprole) //if they are an antagonist of some sort.
+ if(temprole in total_antagonists) //If the role exists already, add the name to it
+ total_antagonists[temprole] += ", [Mind.name]([Mind.key])"
+ else
+ total_antagonists.Add(temprole) //If the role doesnt exist in the list, create it and add the mob
+ total_antagonists[temprole] += ": [Mind.name]([Mind.key])"
+
+ CHECK_TICK
+
+ //Now print them all into the log!
+ log_game("Antagonists at round end were...")
+ for(var/i in total_antagonists)
+ log_game("[i]s[total_antagonists[i]].")
+
+ CHECK_TICK
+
+ //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()
+ var/list/parts = list()
+ var/station_evacuated = EMERGENCY_ESCAPED_OR_ENDGAMED
+ var/num_survivors = 0
+ var/num_escapees = 0
+ var/num_shuttle_escapees = 0
+
+ //Player status report
+ for(var/i in GLOB.mob_list)
+ var/mob/Player = i
+ if(Player.mind && !isnewplayer(Player))
+ if(Player.stat != DEAD && !isbrain(Player))
+ num_survivors++
+ if(station_evacuated) //If the shuttle has already left the station
+ var/list/area/shuttle_areas
+ if(SSshuttle && SSshuttle.emergency)
+ shuttle_areas = SSshuttle.emergency.shuttle_areas
+ if(Player.onCentCom() || Player.onSyndieBase())
+ num_escapees++
+ if(shuttle_areas[get_area(Player)])
+ num_shuttle_escapees++
+
+ //Round statistics report
+ 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)
+
+ parts += "[GLOB.TAB]Shift Duration: [DisplayTimeText(world.time - SSticker.round_start_time)]"
+ parts += "[GLOB.TAB]Station Integrity: [mode.station_was_nuked ? "Destroyed" : "[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: [num_escapees] ([PERCENT(num_escapees/total_players)]%)"
+ parts += "[GLOB.TAB](on emergency shuttle): [num_shuttle_escapees] ([PERCENT(num_shuttle_escapees/total_players)]%)"
+ parts += "[GLOB.TAB]Survival Rate: [num_survivors] ([PERCENT(num_survivors/total_players)]%)"
+ return parts.Join("
")
+
+/datum/controller/subsystem/ticker/proc/show_roundend_report(client/C,common_report)
+ var/list/report_parts = list()
+
+ report_parts += personal_report(C)
+ report_parts += common_report
+
+ var/datum/browser/roundend_report = new(C, "roundend")
+ roundend_report.width = 800
+ roundend_report.height = 600
+ roundend_report.set_content(report_parts.Join())
+ 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)
+ 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 += "