From ea28a87ca8e5d2f8a4fd3a3767d2f0e0fb65acc5 Mon Sep 17 00:00:00 2001
From: John Willard <53777086+JohnFulpWillard@users.noreply.github.com>
Date: Mon, 21 Apr 2025 20:20:41 -0400
Subject: [PATCH] Adds map feedback thread support (AI stat panel buff)
(#90506)
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
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.
:cl:
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).
/:cl:
---
code/__HELPERS/roundend.dm | 1 +
.../configuration/configuration.dm | 3 +
.../configuration/entries/general.dm | 4 ++
code/controllers/subsystem/mapping.dm | 14 +++++
code/controllers/subsystem/statpanel.dm | 28 ++++++++--
code/datums/map_config.dm | 19 +++++--
code/modules/client/client_procs.dm | 7 +++
code/modules/escape_menu/details.dm | 2 +-
code/modules/mob/living/silicon/ai/ai.dm | 15 ++++-
code/modules/mob/mob.dm | 1 +
code/modules/pai/pai.dm | 2 +-
config/maps.txt | 2 +
html/statbrowser.js | 55 +++++++++++++++----
13 files changed, 129 insertions(+), 24 deletions(-)
diff --git a/code/__HELPERS/roundend.dm b/code/__HELPERS/roundend.dm
index f58bba4bd90..c0de1b973a6 100644
--- a/code/__HELPERS/roundend.dm
+++ b/code/__HELPERS/roundend.dm
@@ -353,6 +353,7 @@ GLOBAL_LIST_INIT(achievements_unlocked, list())
var/statspage = CONFIG_GET(string/roundstatsurl)
var/info = statspage ? "[GLOB.round_id]" : GLOB.round_id
parts += "[FOURSPACES]Round ID: [info]"
+ parts += "[FOURSPACES]Map: [SSmapping.current_map?.return_map_name()]"
parts += "[FOURSPACES]Shift Duration: [DisplayTimeText(world.time - SSticker.round_start_time)]"
parts += "[FOURSPACES]Station Integrity: [GLOB.station_was_nuked ? span_redtext("Destroyed") : "[popcount["station_integrity"]]%"]"
var/total_players = GLOB.joined_player_list.len
diff --git a/code/controllers/configuration/configuration.dm b/code/controllers/configuration/configuration.dm
index 6a43e92ae88..90263bb6ecd 100644
--- a/code/controllers/configuration/configuration.dm
+++ b/code/controllers/configuration/configuration.dm
@@ -434,6 +434,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]'")
diff --git a/code/controllers/configuration/entries/general.dm b/code/controllers/configuration/entries/general.dm
index 739877fe125..d244c539f6d 100644
--- a/code/controllers/configuration/entries/general.dm
+++ b/code/controllers/configuration/entries/general.dm
@@ -788,3 +788,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 = ""
diff --git a/code/controllers/subsystem/mapping.dm b/code/controllers/subsystem/mapping.dm
index f29b5853465..d9e947f7c2a 100644
--- a/code/controllers/subsystem/mapping.dm
+++ b/code/controllers/subsystem/mapping.dm
@@ -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()
@@ -958,3 +961,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 = "[map_name]"
+ else
+ text = map_name
+ if(webmap_included && !isnull(SSmapping.current_map.mapping_url))
+ text += " | (Show Map)"
+ return text
diff --git a/code/controllers/subsystem/statpanel.dm b/code/controllers/subsystem/statpanel.dm
index ffec9bb8703..2494afd2281 100644
--- a/code/controllers/subsystem/statpanel.dm
+++ b/code/controllers/subsystem/statpanel.dm
@@ -22,14 +22,25 @@ SUBSYSTEM_DEF(statpanels)
num_fires++
var/datum/map_config/cached = SSmap_vote.next_map_config
/* SKYRAT EDIT CHANGE
- 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)]%)",
)
*/
var/round_time = world.time - SSticker.round_start_time
@@ -123,6 +134,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
diff --git a/code/datums/map_config.dm b/code/datums/map_config.dm
index ce4685b3808..1cde722361a 100644
--- a/code/datums/map_config.dm
+++ b/code/datums/map_config.dm
@@ -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_skyrat",
"ferry" = "ferry_fancy",
"whiteship" = "whiteship_meta",
- "emergency" = "emergency_skyrat") //SKYRAT EDIT CHANGE
+ "emergency" = "emergency_skyrat", //SKYRAT EDIT CHANGE
+ )
/// 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; }
diff --git a/code/modules/client/client_procs.dm b/code/modules/client/client_procs.dm
index e10e7bea96a..dd9dff55f0e 100644
--- a/code/modules/client/client_procs.dm
+++ b/code/modules/client/client_procs.dm
@@ -144,6 +144,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))
diff --git a/code/modules/escape_menu/details.dm b/code/modules/escape_menu/details.dm
index 8f35781b8ab..00267ecba5d 100644
--- a/code/modules/escape_menu/details.dm
+++ b/code/modules/escape_menu/details.dm
@@ -35,7 +35,7 @@ GLOBAL_DATUM(escape_menu_details, /atom/movable/screen/escape_menu/details)
Round ID: [GLOB.round_id || "Unset"]
Round Time: [ROUND_TIME()]
- Map: [SSmapping.current_map.map_name || "Loading..."]
+ Map: [SSmapping.current_map.return_map_name(webmap_included = TRUE) || "Loading..."]
Time Dilation: [round(SStime_track.time_dilation_current,1)]%
"}
diff --git a/code/modules/mob/living/silicon/ai/ai.dm b/code/modules/mob/living/silicon/ai/ai.dm
index 3136f568572..7b95a643648 100644
--- a/code/modules/mob/living/silicon/ai/ai.dm
+++ b/code/modules/mob/living/silicon/ai/ai.dm
@@ -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."))
diff --git a/code/modules/mob/mob.dm b/code/modules/mob/mob.dm
index dc5b53322ae..8da5076ec1a 100644
--- a/code/modules/mob/mob.dm
+++ b/code/modules/mob/mob.dm
@@ -889,6 +889,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
diff --git a/code/modules/pai/pai.dm b/code/modules/pai/pai.dm
index 94b6f38067a..f77d327d114 100644
--- a/code/modules/pai/pai.dm
+++ b/code/modules/pai/pai.dm
@@ -174,7 +174,7 @@
//SKYRAT EDIT ADDITION END
/mob/living/silicon/pai/get_status_tab_items()
- . += ..()
+ . = ..()
if(!stat)
. += "Emitter Integrity: [holochassis_health * (100 / HOLOCHASSIS_MAX_HEALTH)]."
else
diff --git a/config/maps.txt b/config/maps.txt
index b3da0791147..3b32d58cbc9 100644
--- a/config/maps.txt
+++ b/config/maps.txt
@@ -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.
@@ -33,6 +34,7 @@ map metastation
minplayers 25
#voteweight 0.5
votable
+ #feedbacklink https://www.youtube.com/watch?v=XG8b7WhANNA
endmap
map tramstation
diff --git a/html/statbrowser.js b/html/statbrowser.js
index d814f44d8b3..2e6a3b1b42e 100644
--- a/html/statbrowser.js
+++ b/html/statbrowser.js
@@ -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();