mirror of
https://github.com/SPLURT-Station/S.P.L.U.R.T-Station-13.git
synced 2025-12-09 16:07:40 +00:00
796 lines
31 KiB
Plaintext
796 lines
31 KiB
Plaintext
#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, "<BR><BR><BR><span class='big bold'>The round has ended.</span>")
|
|
log_game("The round has ended.")
|
|
|
|
CONFIG_SET(flag/suicide_allowed,TRUE) // EORG suicides allowed
|
|
|
|
for(var/I in round_end_events)
|
|
var/datum/callback/cb = I
|
|
cb.InvokeAsync()
|
|
LAZYCLEARLIST(round_end_events)
|
|
|
|
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.
|
|
if(CONFIG_GET(string/chat_roundend_notice_tag))
|
|
var/broadcastmessage = ""
|
|
|
|
if(LAZYLEN(GLOB.round_end_notifiees))
|
|
broadcastmessage += "[GLOB.round_end_notifiees.Join(", ")], "
|
|
|
|
|
|
broadcastmessage += "[((broadcastmessage == "") ? "the" : "The")] current round has ended. Please standby for your shift interlude Nanotrasen News Network's report!\n"
|
|
broadcastmessage += "```\n[send_news_report()]\n```"
|
|
|
|
if(CONFIG_GET(string/chat_reboot_role))
|
|
broadcastmessage += "\n\n<@&[CONFIG_GET(string/chat_reboot_role)]>, the server will reboot shortly!"
|
|
|
|
send2chat(broadcastmessage, CONFIG_GET(string/chat_roundend_notice_tag))
|
|
|
|
CHECK_TICK
|
|
|
|
// handle_hearts()
|
|
set_observer_default_invisibility(0, "<span class='warning'>The round is over! You are now visible to the living.</span>")
|
|
|
|
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
|
|
var/datum/game_mode/dynamic/mode = SSticker.mode
|
|
|
|
if(GLOB.round_id)
|
|
var/statspage = CONFIG_GET(string/roundstatsurl)
|
|
var/info = statspage ? "<a href='?action=openLink&link=[url_encode(statspage)][GLOB.round_id]'>[GLOB.round_id]</a>" : GLOB.round_id
|
|
parts += "[FOURSPACES]Round ID: <b>[info]</b>"
|
|
|
|
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: <B>[DisplayTimeText(world.time - SSticker.round_start_time)]</B>"
|
|
parts += "[FOURSPACES]Station Integrity: <B>[mode.station_was_nuked ? "<span class='redtext'>Destroyed</span>" : "[popcount["station_integrity"]]%"]</B>"
|
|
var/total_players = GLOB.joined_player_list.len
|
|
if(total_players)
|
|
parts+= "[FOURSPACES]Total Population: <B>[total_players]</B>"
|
|
if(station_evacuated)
|
|
parts += "<BR>[FOURSPACES]Evacuation Rate: <B>[popcount[POPCOUNT_ESCAPEES]] ([PERCENT(popcount[POPCOUNT_ESCAPEES]/total_players)]%)</B>"
|
|
parts += "[FOURSPACES](on emergency shuttle): <B>[popcount[POPCOUNT_SHUTTLE_ESCAPEES]] ([PERCENT(popcount[POPCOUNT_SHUTTLE_ESCAPEES]/total_players)]%)</B>"
|
|
parts += "[FOURSPACES]Survival Rate: <B>[popcount[POPCOUNT_SURVIVORS]] ([PERCENT(popcount[POPCOUNT_SURVIVORS]/total_players)]%)</B>"
|
|
if(SSblackbox.first_death)
|
|
var/list/ded = SSblackbox.first_death
|
|
if(ded.len)
|
|
parts += "[FOURSPACES]First Death: <b>[ded["name"]], [ded["role"]], at [ded["area"]]. Damage taken: [ded["damage"]].[ded["last_words"] ? " Their last words were: \"[ded["last_words"]]\"" : ""]</b>"
|
|
//ignore this comment, it fixes the broken sytax parsing caused by the " above
|
|
else
|
|
parts += "[FOURSPACES]<i>Nobody died this shift!</i>"
|
|
if(istype(SSticker.mode, /datum/game_mode/dynamic))
|
|
parts += "[FOURSPACES]Initial threat level: [mode.threat_level]"
|
|
parts += "[FOURSPACES]Initial roundstart threat: [mode.initial_round_start_budget]"
|
|
parts += "[FOURSPACES]Roundstart budget after antags: [mode.round_start_budget]"
|
|
parts += "[FOURSPACES]Midround budget at round end: [mode.mid_round_budget]"
|
|
parts += "[FOURSPACES]Executed rules:"
|
|
for(var/datum/dynamic_ruleset/rule in mode.executed_rules)
|
|
parts += "[FOURSPACES][FOURSPACES][rule.ruletype] - <b>[rule.name]</b>: -[rule.cost + rule.scaled_times * rule.scaling_cost] threat"
|
|
return parts.Join("<br>")
|
|
|
|
/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 += "<div class='panel stationborder'>"
|
|
parts += GLOB.survivor_report
|
|
parts += "</div>"
|
|
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 += "<div class='panel stationborder'>"
|
|
parts += "<span class='marooned'>You managed to survive, but were marooned on [station_name()]...</span>"
|
|
else
|
|
parts += "<div class='panel greenborder'>"
|
|
parts += "<span class='greentext'>You managed to survive the events on [station_name()] as [M.real_name].</span>"
|
|
else
|
|
parts += "<div class='panel greenborder'>"
|
|
parts += "<span class='greentext'>You managed to survive the events on [station_name()] as [M.real_name].</span>"
|
|
|
|
else
|
|
parts += "<div class='panel redborder'>"
|
|
parts += "<span class='redtext'>You did not survive the events on [station_name()]...</span>"
|
|
else
|
|
parts += "<div class='panel stationborder'>"
|
|
parts += "<br>"
|
|
parts += GLOB.survivor_report
|
|
parts += "</div>"
|
|
|
|
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 += "<b>[aiPlayer.name]</b>[aiPlayer.mind.hide_ckey ? "" : " (Played by: <b>[aiPlayer.mind.key]</b>)"]'s laws [aiPlayer.stat != DEAD ? "at the end of the round" : "when it was <span class='redtext'>deactivated</span>"] were:"
|
|
parts += aiPlayer.laws.get_law_list(include_zeroth=TRUE)
|
|
|
|
parts += "<b>Total law changes: [aiPlayer.law_change_counter]</b>"
|
|
|
|
if (aiPlayer.connected_robots.len)
|
|
var/borg_num = aiPlayer.connected_robots.len
|
|
parts += "<br><b>[aiPlayer.real_name]</b>'s minions were:"
|
|
for(var/mob/living/silicon/robot/robo in aiPlayer.connected_robots)
|
|
borg_num--
|
|
if(robo.mind)
|
|
parts += "<b>[robo.name]</b>[robo.mind.hide_ckey ? "" : " (Played by: <b>[robo.mind.key]</b>)"] [robo.stat == DEAD ? " <span class='redtext'>(Deactivated)</span>" : ""][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?"<br>":""]<b>[robo.name]</b>[robo.mind.hide_ckey ? "" : " (Played by: <b>[robo.mind.key]</b>)"] [(robo.stat != DEAD)? "<span class='greentext'>survived</span> as an AI-less borg!" : "was <span class='redtext'>unable to survive</span> 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 "<div class='panel stationborder'>[parts.Join("<br>")]</div>"
|
|
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 "<div class='panel stationborder'><ul>[parts.Join()]</ul></div>"
|
|
|
|
///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()
|
|
|
|
///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/static/list/typecache_bank = typecacheof(list(/datum/bank_account/department, /datum/bank_account/remote))
|
|
for(var/i in SSeconomy.generated_accounts)
|
|
var/datum/bank_account/current_acc = SSeconomy.generated_accounts[i]
|
|
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 += "<div class='panel stationborder'><span class='header'>Station Economic Summary:</span><br>"
|
|
parts += "<b>General Statistics:</b><br>"
|
|
parts += "There were [station_vault] credits collected by crew this shift.<br>"
|
|
if(total_players > 0)
|
|
parts += "An average of [station_vault/total_players] credits were collected.<br>"
|
|
// 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 <b>[mr_moneybags.account_holder] with [mr_moneybags.account_balance]</b> cr!</div>"
|
|
else
|
|
parts += "Somehow, nobody made any money this shift! This'll result in some budget cuts...</div>"
|
|
return parts
|
|
|
|
/datum/controller/subsystem/ticker/proc/medal_report()
|
|
if(GLOB.commendations.len)
|
|
var/list/parts = list()
|
|
parts += "<span class='header'>Medal Commendations:</span>"
|
|
for (var/com in GLOB.commendations)
|
|
parts += com
|
|
return "<div class='panel stationborder'>[parts.Join("<br>")]</div>"
|
|
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
|
|
. += "<div class='panel stationborder'><span class='header'>The following people made it out as a random hardcore character:</span>"
|
|
. += "<ul class='playerlist'>"
|
|
for(var/mob/living/carbon/human/human_player in hardcores)
|
|
. += "<li>[printplayer(human_player.mind)] with a hardcore random score of [round(human_player.hardcore_survival_score)]</li>"
|
|
. += "</ul></div>"
|
|
|
|
/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 += "</div>"
|
|
result += "<div class='panel redborder'>"
|
|
result += A.roundend_report_header()
|
|
currrent_category = A.roundend_category
|
|
previous_category = A
|
|
result += A.roundend_report()
|
|
//ambition start
|
|
for(var/count in 1 to LAZYLEN(A.owner.ambitions))
|
|
result += "<br><B>Ambition #[count]</B>: [A.owner.ambitions[count]]"
|
|
//ambition end
|
|
result += "<br><br>"
|
|
CHECK_TICK
|
|
|
|
if(all_antagonists.len)
|
|
var/datum/antagonist/last = all_antagonists[all_antagonists.len]
|
|
result += last.roundend_report_footer()
|
|
result += "</div>"
|
|
|
|
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,"<a href='?src=[REF(R)];report=1'>Show roundend report again</a>")
|
|
|
|
/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 <b>[ply.assigned_role]</b>"
|
|
var/text = (ply.hide_ckey ? \
|
|
"<b>[ply.key]</b> was <b>[ply.name]</b>[jobtext] and" \
|
|
: "<b>[ply.name]</b>[jobtext]")
|
|
if(ply.current)
|
|
if(ply.current.stat == DEAD)
|
|
text += " <span class='redtext'>died</span>"
|
|
else
|
|
text += " <span class='greentext'>survived</span>"
|
|
if(fleecheck)
|
|
var/turf/T = get_turf(ply.current)
|
|
if(!T || !is_station_level(T.z))
|
|
text += " while <span class='redtext'>fleeing the station</span>"
|
|
if(ply.current.real_name != ply.name)
|
|
text += " as <b>[ply.current.real_name]</b>"
|
|
else
|
|
text += " <span class='redtext'>had their body destroyed</span>"
|
|
return text
|
|
|
|
/proc/printplayerlist(list/players,fleecheck)
|
|
var/list/parts = list()
|
|
|
|
parts += "<ul class='playerlist'>"
|
|
for(var/datum/mind/M in players)
|
|
parts += "<li>[printplayer(M,fleecheck)]</li>"
|
|
parts += "</ul>"
|
|
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 += "<B>Objective #[count]</B>: [objective.explanation_text] <span class='greentext'><B>Success!</B></span>"
|
|
else if(completion <= 0)
|
|
objective_parts += "<B>Objective #[count]</B>: [objective.explanation_text] <span class='redtext'>Fail.</span>"
|
|
else
|
|
objective_parts += "<B>Objective #[count]</B>: [objective.explanation_text] <span class='yellowtext'>[completion*100]%</span>"
|
|
else
|
|
objective_parts += "<B>Objective #[count]</B>: [objective.explanation_text]"
|
|
count++
|
|
return objective_parts.Join("<br>")
|
|
|
|
/datum/controller/subsystem/ticker/proc/save_admin_data()
|
|
if(IsAdminAdvancedProcCall())
|
|
to_chat(usr, "<span class='admin prefix'>Admin rank DB Sync blocked: Advanced ProcCall detected.</span>")
|
|
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)
|