Adds map feedback thread support (AI stat panel buff) (#90506)

## About The Pull Request

This PR started with the idea of adding support for map feedback
threads, which I added to the roundend report, escape menu, and stat
panel. To do this though I had to make pretty annoying changes to the
stat panel and had to touch every single time something to the stat
panel was added, so since we now have a way to have links in the stat
panel I thought of taking full advantage of it and add some QOL.

AIs can now track their borgs by clicking their status on the stat panel


https://github.com/user-attachments/assets/1789dc46-5d12-48e9-bb8d-d3278aa19639

With Melbert's comment, I added another stat panel entry that directs
you to the Webmap page, which currently seems to be a little messed up
(https://github.com/AffectedArc07/SS13WebMap/issues/41 &
https://github.com/AffectedArc07/SS13WebMap/issues/42) but if they get
fixed this would be a swag asf feature

##### Code bounty for Ezel/Improvedname

## Why It's Good For The Game

Feedback threads was a suggestion from a player and is fully in control
of admins as an optional thing, and while we still have stat panel I
think it's nice to be able to take advantage of its features.

## Changelog

🆑
admin: Admins can now link a URL for maps, used to give feedback on said
maps. Accessible through the roundend report, escape menu, and stat
panel.
qol: AIs can track their borgs by clicking on them in the stat panel.
qol: You can now directly go to the webmap of maps from the stat panel
(assuming it's set in config).
/🆑
This commit is contained in:
John Willard
2025-04-21 20:20:41 -04:00
committed by GitHub
parent c7fc004236
commit 380c143431
13 changed files with 130 additions and 24 deletions

View File

@@ -341,6 +341,7 @@ GLOBAL_LIST_INIT(achievements_unlocked, list())
var/statspage = CONFIG_GET(string/roundstatsurl)
var/info = statspage ? "<a href='byond://?action=openLink&link=[url_encode(statspage)][GLOB.round_id]'>[GLOB.round_id]</a>" : GLOB.round_id
parts += "[FOURSPACES]Round ID: <b>[info]</b>"
parts += "[FOURSPACES]Map: [SSmapping.current_map?.return_map_name()]"
parts += "[FOURSPACES]Shift Duration: <B>[DisplayTimeText(world.time - SSticker.round_start_time)]</B>"
parts += "[FOURSPACES]Station Integrity: <B>[GLOB.station_was_nuked ? span_redtext("Destroyed") : "[popcount["station_integrity"]]%"]</B>"
var/total_players = GLOB.joined_player_list.len

View File

@@ -432,6 +432,9 @@ Example config:
currentmap = null
if ("disabled")
currentmap = null
if("feedbacklink")
if(currentmap.map_name == SSmapping.current_map.map_name)
SSmapping.current_map.feedback_link = data
else
log_config("Unknown command in map vote config: '[command]'")

View File

@@ -785,3 +785,7 @@
// If set, enables the "Link forum account" OOC verb
/datum/config_entry/string/forum_link_uri
/datum/config_entry/string/webmap_url
//ex: "https://webmap.affectedarc07.co.uk/maps/tgstation/"
default = ""

View File

@@ -106,6 +106,9 @@ SUBSYSTEM_DEF(mapping)
if(!current_map || current_map.defaulted)
to_chat(world, span_boldannounce("Unable to load next or default map config, defaulting to [old_config.map_name]."))
current_map = old_config
var/mapping_url = config.Get(/datum/config_entry/string/webmap_url)
if(mapping_url != "")
current_map.mapping_url = mapping_url
plane_offset_to_true = list()
true_to_offset_planes = list()
plane_to_offset = list()
@@ -950,3 +953,14 @@ ADMIN_VERB(load_away_mission, R_FUN, "Load Away Mission", "Load a specific away
var/number_of_remaining_levels = length(checkable_levels)
if(number_of_remaining_levels > 0)
CRASH("The following [number_of_remaining_levels] away mission(s) were not loaded: [checkable_levels.Join("\n")]")
///Returns the map name, with an openlink action tied to it (if one exists) for the map.
/datum/map_config/proc/return_map_name(webmap_included)
var/text
if(feedback_link)
text = "<a href='byond://?action=openLink&link=[url_encode(feedback_link)]'>[map_name]</a>"
else
text = map_name
if(webmap_included && !isnull(SSmapping.current_map.mapping_url))
text += " | <a href='byond://?action=openWebMap'>(Show Map)</a>"
return text

View File

@@ -21,14 +21,26 @@ SUBSYSTEM_DEF(statpanels)
if (!resumed)
num_fires++
var/datum/map_config/cached = SSmap_vote.next_map_config
global_data = list(
"Map: [SSmapping.current_map?.map_name || "Loading..."]",
cached ? "Next Map: [cached.map_name]" : null,
if(isnull(SSmapping.current_map))
global_data = list("Loading")
else if(SSmapping.current_map.feedback_link)
global_data = list(list("Map: [SSmapping.current_map.map_name]", " (Feedback)", "action=openLink&link=[SSmapping.current_map.feedback_link]"))
else
global_data = list("Map: [SSmapping.current_map?.map_name]")
if(SSmapping.current_map?.mapping_url)
global_data += list(list("same_line", " | (View in Browser)", "action=openWebMap"))
if(cached)
global_data += "Next Map: [cached.map_name]"
global_data += list(
"Round ID: [GLOB.round_id ? GLOB.round_id : "NULL"]",
"Server Time: [time2text(world.timeofday, "YYYY-MM-DD hh:mm:ss", world.timezone)]",
"Round Time: [ROUND_TIME()]",
"Station Time: [station_time_timestamp()]",
"Time Dilation: [round(SStime_track.time_dilation_current,1)]% AVG:([round(SStime_track.time_dilation_avg_fast,1)]%, [round(SStime_track.time_dilation_avg,1)]%, [round(SStime_track.time_dilation_avg_slow,1)]%)"
"Time Dilation: [round(SStime_track.time_dilation_current,1)]% AVG:([round(SStime_track.time_dilation_avg_fast,1)]%, [round(SStime_track.time_dilation_avg,1)]%, [round(SStime_track.time_dilation_avg_slow,1)]%)",
)
if(SSshuttle.emergency)
@@ -98,6 +110,15 @@ SUBSYSTEM_DEF(statpanels)
if(MC_TICK_CHECK)
return
/*
* send_message for the stat panel can be sent 1 of 4 things:
* 1- A string entry, to show up as plain text.
* 2- An empty string (""), which will translate to a new line, to for a break between lines.
* 3- a list, in which the first entry is plain text, the second entry is highlighted text, and the third entry is a link
* that clicking the second entry will take you to.
* 4- a list with "same_line" as the first entry, which will automatically put it on the line above it,
* with the second/third entry matching #3 (text & url), allowing you to have 2 clickable links on one line.
*/
/datum/controller/subsystem/statpanels/proc/set_status_tab(client/target)
if(!global_data)//statbrowser hasnt fired yet and we were called from immediate_send_stat_data()
return

View File

@@ -13,6 +13,12 @@
var/voteweight = 1
var/votable = FALSE
///A URL linking to a place for people to send feedback about this map.
var/feedback_link
/// The URL given by config directing you to the webmap.
var/mapping_url
// Config actually from the JSON - should default to Meta
var/map_name = "MetaStation"
var/map_path = "map_files/MetaStation"
@@ -34,7 +40,8 @@
"cargo" = "cargo_box",
"ferry" = "ferry_fancy",
"whiteship" = "whiteship_meta",
"emergency" = "emergency_meta")
"emergency" = "emergency_meta",
)
/// Dictionary of job sub-typepath to template changes dictionary
var/job_changes = list()
@@ -66,7 +73,7 @@
* Returns the config for the map to load.
*/
/proc/load_map_config(filename = null, directory = null, error_if_missing = TRUE)
var/datum/map_config/config = load_default_map_config()
var/datum/map_config/configuring_map = load_default_map_config()
if(filename) // If none is specified, then go to look for next_map.json, for map rotation purposes.
@@ -74,7 +81,7 @@
if(directory)
if(!(directory in MAP_DIRECTORY_WHITELIST))
log_world("map directory not in whitelist: [directory] for map [filename]")
return config
return configuring_map
else
directory = MAP_DIRECTORY_MAPS
@@ -83,10 +90,10 @@
filename = PATH_TO_NEXT_MAP_JSON
if (!config.LoadConfig(filename, error_if_missing))
qdel(config)
if (!configuring_map.LoadConfig(filename, error_if_missing))
qdel(configuring_map)
return load_default_map_config()
return config
return configuring_map
#define CHECK_EXISTS(X) if(!istext(json[X])) { log_world("[##X] missing from json!"); return; }

View File

@@ -135,6 +135,13 @@ GLOBAL_LIST_INIT(blacklisted_builds, list(
switch(href_list["action"])
if("openLink")
src << link(href_list["link"])
if("openWebMap")
if(!SSmapping.current_map.mapping_url)
return
if(is_station_level(mob.z))
src << link("[SSmapping.current_map.mapping_url][LOWER_TEXT(sanitize_css_class_name(SSmapping.current_map.map_name))]/?x=[mob.x]&y=[mob.y]&zoom=6")
else
src << link("[SSmapping.current_map.mapping_url][LOWER_TEXT(sanitize_css_class_name(SSmapping.current_map.map_name))]")
if (hsrc)
var/datum/real_src = hsrc
if(QDELETED(real_src))

View File

@@ -35,7 +35,7 @@ GLOBAL_DATUM(escape_menu_details, /atom/movable/screen/escape_menu/details)
<span style='text-align: right; line-height: 0.7'>
Round ID: [GLOB.round_id || "Unset"]<br />
Round Time: [ROUND_TIME()]<br />
Map: [SSmapping.current_map.map_name || "Loading..."]<br />
Map: [SSmapping.current_map.return_map_name(webmap_included = TRUE) || "Loading..."]<br />
Time Dilation: [round(SStime_track.time_dilation_current,1)]%<br />
</span>
"}

View File

@@ -234,8 +234,13 @@
else if(!connected_robot.cell || connected_robot.cell.charge <= 0)
robot_status = "DEPOWERED"
//Name, Health, Battery, Model, Area, and Status! Everything an AI wants to know about its borgies!
. += "[connected_robot.name] | S.Integrity: [connected_robot.health]% | Cell: [connected_robot.cell ? "[display_energy(connected_robot.cell.charge)]/[display_energy(connected_robot.cell.maxcharge)]" : "Empty"] | \
Model: [connected_robot.designation] | Loc: [get_area_name(connected_robot, TRUE)] | Status: [robot_status]"
. += list(list("[connected_robot.name]: ",
"S.Integrity: [connected_robot.health]% | \
Cell: [connected_robot.cell ? "[display_energy(connected_robot.cell.charge)]/[display_energy(connected_robot.cell.maxcharge)]" : "Empty"] | \
Model: [connected_robot.designation] | Loc: [get_area_name(connected_robot, TRUE)] | \
Status: [robot_status]",
"src=[REF(src)];track_cyborg=[text_ref(connected_robot)]",
))
. += "AI shell beacons detected: [LAZYLEN(GLOB.available_ai_shells)]" //Count of total AI shells
/mob/living/silicon/ai/proc/ai_call_shuttle()
@@ -390,6 +395,12 @@
if(usr != src)
return
if(href_list["track_cyborg"])
var/mob/living/silicon/robot/cyborg = locate(href_list["track_cyborg"]) in connected_robots
if(!cyborg)
return
ai_tracking_tool.set_tracked_mob(cyborg)
if(href_list["emergencyAPC"]) //This check comes before incapacitated because the only time it would be useful is when we have no power.
if(!apc_override)
to_chat(src, span_notice("APC backdoor is no longer available."))

View File

@@ -838,6 +838,7 @@
/mob/proc/get_status_tab_items()
. = list("") //we want to offset unique stuff from standard stuff
SEND_SIGNAL(src, COMSIG_MOB_GET_STATUS_TAB_ITEMS, .)
return .
/**
* Convert a list of spells into a displyable list for the statpanel

View File

@@ -171,7 +171,7 @@
. += "Its master ID string seems to be [(!master_name || emagged) ? "empty" : master_name]."
/mob/living/silicon/pai/get_status_tab_items()
. += ..()
. = ..()
if(!stat)
. += "Emitter Integrity: [holochassis_health * (100 / HOLOCHASSIS_MAX_HEALTH)]."
else

View File

@@ -11,6 +11,7 @@ Format:
voteweight [number] (How much to count each player vote as, defaults to 1, setting to 0.5 counts each vote as half a vote, 2 as double, etc, Setting to 0 disables the map but allows players to still pick it)
disabled (disables the map)
votable (is this map votable)
feedbackurl (link in-game shown to players to leave feedback for the map)
endmap
# Production-level maps.
@@ -35,6 +36,7 @@ map metastation
minplayers 25
#voteweight 0.5
votable
#feedbacklink https://www.youtube.com/watch?v=XG8b7WhANNA
endmap
map tramstation

View File

@@ -15,9 +15,13 @@ if (!String.prototype.trim) {
}
// Status panel implementation ------------------------------------------------
var status_tab_parts = ["Loading..."];
//status_tab_parts expects a list to be returned, to which we'll send a list within a list
//with just "loading" to not appear broken.
var status_tab_parts = [["Loading..."]];
var current_tab = null;
var mc_tab_parts = [["Loading...", ""]];
//mc_tab_parts expects a list to be returned, to which we'll send a list within a list
//with just "loading" to not appear broken.
var mc_tab_parts = [["Loading..."]];
var href_token = null;
var spells = [];
var spell_tabs = [];
@@ -346,16 +350,42 @@ function draw_status() {
current_tab = "Status";
}
statcontentdiv.textContent = '';
var table = document.createElement("table");
for (var i = 0; i < status_tab_parts.length; i++) {
if (status_tab_parts[i].trim() == "") {
document.getElementById("statcontent").appendChild(document.createElement("br"));
} else {
var part = status_tab_parts[i];
if(!Array.isArray(part)) {
var div = document.createElement("div");
div.textContent = status_tab_parts[i];
div.className = "status-info";
document.getElementById("statcontent").appendChild(div);
if (part.trim() == "") {
table.appendChild(document.createElement("br"));
} else {
div.textContent = part;
table.appendChild(div);
}
} else {
var div
if (part[0].trim() == "same_line") {
var a = document.createElement("a");
a.href = "byond://?" + part[2];
a.textContent = part[1];
div.appendChild(a);
} else {
div = document.createElement("div");
if (part[0].trim() == "") {
table.appendChild(document.createElement("br"));
} else {
div.textContent = part[0];
if (part[2]) {
var a = document.createElement("a");
a.href = "byond://?" + part[2];
a.textContent = part[1];
div.appendChild(a);
}
table.appendChild(div);
}
}
}
}
document.getElementById("statcontent").appendChild(table);
if (verb_tabs.length == 0 || !verbs) {
Byond.command("Fix-Stat-Panel");
}
@@ -876,13 +906,18 @@ Byond.subscribeTo('init_verbs', function (payload) {
Byond.subscribeTo('update_stat', function (payload) {
status_tab_parts = [payload.ping_str];
var parsed = payload.global_data;
for (var i = 0; i < parsed.length; i++) if (parsed[i] != null) status_tab_parts.push(parsed[i]);
for (var i = 0; i < parsed.length; i++)
if (parsed[i] != null)
status_tab_parts.push(parsed[i]);
parsed = payload.other_str;
for (var i = 0; i < parsed.length; i++) if (parsed[i] != null) status_tab_parts.push(parsed[i]);
for (var i = 0; i < parsed.length; i++)
if (parsed[i] != null)
status_tab_parts.push(parsed[i]);
if (current_tab == "Status") {
draw_status();