Pathfinding Visualization, JPS fixes, Misc Improvement (#90233)

## About The Pull Request

[cleans up poor namespacing on light debugging
tools](https://github.com/tgstation/tgstation/commit/93cc9070d56ee4dbeb600ebd4616b047adf9d648)

[Implements a pathfinding visualization
tool](https://github.com/tgstation/tgstation/commit/ed91f69ac42a92329c10ccdbba308d213fc8c759)

It holds a set of inputs from the client, and uses them to generate and
display paths from source/target. Left click sets the source, right
click sets the target.

Pathmap support too, if no target is set we display the paths from every
turf in the map to the source, if one is set we build a path TO it from
the source.

I had to add COMSIG_MOB_CLICKON to observers to make this work (tho idk
why it didn't exist already), I also removed the everpresent colorblind
datum from admin datums, only needs to exist if they're using it.

[Adds a mutable_appearance helper that dirlocks them, wallening port
which I thought might be useful here, and will likely be useful
elsewhere in
future](https://github.com/tgstation/tgstation/commit/87f752e7c361ff47d62452f2f1ea805458f71b66)

[Fixes an infinite loop in pathmaps if we tried to pull a cached path to
an unreachable target, && not ||
4head](https://github.com/tgstation/tgstation/commit/10086a655df817c9f4494f9aac879c85a6059719)

[Fixes JPS not dealing with border objects properly. They violate some
of the assumptions JPS makes, so we need to backfill them with checks.
These basically read as (if the thing that should normally take this
path can't reach this turf, can
we?)](https://github.com/tgstation/tgstation/commit/f56cc4dd434cecee89879aea500bd1c9d2f38ce6)

## Why It's Good For The Game

Maybe deals with #80619?

Adds more robust testing tools for pathfinding, should allow people to
better understand/debug these systems. I added this with the idea of
adding multiz support but I don't have the time for that rn.

JPS will work around thindows better, that's nice.

https://file.house/IrBiR0bGxoKw1jJJoxgMRQ==.mp4

## Changelog
🆑
fix: Fixed our pathfinding logic getting deeply confused by border
objects
admin: Added clientside displayed pathfinding debug tools, give em a go
/🆑
This commit is contained in:
LemonInTheDark
2025-03-28 01:51:57 +02:00
committed by GitHub
parent b18be2e531
commit 77823ad210
15 changed files with 328 additions and 22 deletions
+5 -2
View File
@@ -33,9 +33,10 @@ GLOBAL_PROTECT(href_token)
var/datum/filter_editor/filteriffic
var/datum/particle_editor/particle_test
var/datum/colorblind_tester/color_test = new
var/datum/colorblind_tester/color_test
var/datum/plane_master_debug/plane_debug
var/obj/machinery/computer/libraryconsole/admin_only_do_not_map_in_you_fucker/library_manager
var/datum/pathfind_debug/path_debug
/// Whether or not the user tried to connect, but was blocked by 2FA
var/blocked_by_2fa = FALSE
@@ -78,7 +79,8 @@ GLOBAL_PROTECT(href_token)
if(IsAdminAdvancedProcCall())
alert_to_permissions_elevation_attempt(usr)
return QDEL_HINT_LETMELIVE
. = ..()
QDEL_NULL(path_debug)
return ..()
/datum/admins/proc/activate()
if(IsAdminAdvancedProcCall())
@@ -99,6 +101,7 @@ GLOBAL_PROTECT(href_token)
GLOB.deadmins[target] = src
GLOB.admin_datums -= target
QDEL_NULL(plane_debug)
QDEL_NULL(path_debug)
deadmined = TRUE
var/client/client = owner || GLOB.directory[target]
+10
View File
@@ -552,6 +552,8 @@ ADMIN_VERB(init_log, R_DEBUG, "Display Initialize() Log", "Displays a list of th
browser.open()
ADMIN_VERB(debug_color_test, R_DEBUG, "Colorblind Testing", "Change your view to a budget version of colorblindness to test for usability.", ADMIN_CATEGORY_DEBUG)
if(!user.holder.color_test)
user.holder.color_test = new()
user.holder.color_test.ui_interact(user.mob)
ADMIN_VERB(debug_plane_masters, R_DEBUG, "Edit/Debug Planes", "Edit and visualize plane masters and their connections (relays).", ADMIN_CATEGORY_DEBUG)
@@ -654,6 +656,14 @@ ADMIN_VERB(run_empty_query, R_DEBUG, "Run Empty Query", "Runs a specified number
message_admins("[key_name_admin(user)] ran [val] empty queries.")
ADMIN_VERB(test_pathfinding, R_DEBUG, "Toggle Pathfind Testing", "Enables/Disables pathfinding testing action buttons", ADMIN_CATEGORY_DEBUG)
BLACKBOX_LOG_ADMIN_VERB("Toggle Pathfind Testing")
log_admin("[key_name(user)] [user.holder.path_debug ? "disabled" : "enabled"] their pathfinding debug tools")
if(!user.holder.path_debug)
user.holder.path_debug = new(user.holder)
else
QDEL_NULL(user.holder.path_debug)
ADMIN_VERB(clear_turf_reservations, R_DEBUG, "Clear Dynamic Turf Reservations", "Deallocates all reserved space, restoring it to round start conditions.", ADMIN_CATEGORY_DEBUG)
var/answer = tgui_alert(
user,
+6 -6
View File
@@ -1,12 +1,12 @@
/proc/debug_sources()
/proc/debug_light_sources()
GLOB.light_debug_enabled = TRUE
var/list/sum = list()
var/total = 0
for(var/datum/light_source/source)
if(!source.source_atom)
continue
source.source_atom.debug()
source.source_atom.debug_lights()
sum[source.source_atom.type] += 1
total += 1
@@ -29,14 +29,14 @@
var/datum/action/spawn_light/let_there_be = new (new_lad.mob.mind || new_lad.mob)
let_there_be.Grant(new_lad.mob)
/proc/undebug_sources()
/proc/undebug_light_sources()
GLOB.light_debug_enabled = FALSE
for(var/datum/weakref/button_ref as anything in GLOB.light_debugged_atoms)
var/atom/button = button_ref.resolve()
if(!button)
GLOB.light_debugged_atoms -= button_ref
continue
button.undebug()
button.undebug_lights()
SEND_GLOBAL_SIGNAL(COMSIG_LIGHT_DEBUG_DISABLED)
SSdcs.UnregisterSignal(SSdcs, COMSIG_GLOB_CLIENT_CONNECT)
@@ -44,7 +44,7 @@
GLOBAL_LIST_EMPTY(light_debugged_atoms)
/// Sets up this light source to be debugged, setting up in world buttons to control and move it
/// Also freezes it, so it can't change in future
/atom/proc/debug()
/atom/proc/debug_lights()
if(isturf(src) || HAS_TRAIT(src, TRAIT_LIGHTING_DEBUGGED))
return
ADD_TRAIT(src, TRAIT_LIGHTING_DEBUGGED, LIGHT_DEBUG_TRAIT)
@@ -65,7 +65,7 @@ GLOBAL_LIST_EMPTY(light_debugged_atoms)
new /atom/movable/screen/light_button/move(src)
/// Disables light debugging, so you can let a scene fall to what it visually should be, or just fix admin fuckups
/atom/proc/undebug()
/atom/proc/undebug_lights()
// I don't really want to undebug a light if it's off rn
// Loses control if we turn it back on again
if(isturf(src) || !HAS_TRAIT(src, TRAIT_LIGHTING_DEBUGGED) || !light)
+2 -2
View File
@@ -390,7 +390,7 @@ ADMIN_VERB(check_for_obstructed_atmospherics, R_DEBUG, "Check For Obstructed Atm
ADMIN_VERB_VISIBILITY(modify_lights, ADMIN_VERB_VISIBLITY_FLAG_MAPPING_DEBUG)
ADMIN_VERB(modify_lights, R_DEBUG, "Toggle Light Debug", "Toggles light debug mode.", ADMIN_CATEGORY_MAPPING)
if(GLOB.light_debug_enabled)
undebug_sources()
undebug_light_sources()
return
for(var/obj/machinery/light/fix_up as anything in SSmachines.get_machines_by_type_and_subtypes(/obj/machinery/light))
@@ -398,7 +398,7 @@ ADMIN_VERB(modify_lights, R_DEBUG, "Toggle Light Debug", "Toggles light debug mo
if(initial(fix_up.status) == LIGHT_OK)
fix_up.fix()
CHECK_TICK
debug_sources()
debug_light_sources()
ADMIN_VERB_VISIBILITY(visualize_lights, ADMIN_VERB_VISIBLITY_FLAG_MAPPING_DEBUG)
ADMIN_VERB(visualize_lights, R_DEBUG, "Visualize Lighting Corners", "Visualizes the corners of all lights on the station.", ADMIN_CATEGORY_MAPPING)
+263
View File
@@ -0,0 +1,263 @@
GLOBAL_DATUM_INIT(pathfind_dude, /obj/pathfind_guy, new())
/obj/pathfind_guy
/// Enables testing/visualization of pathfinding work
/datum/pathfind_debug
var/datum/admins/owner
var/datum/action/innate/path_debug/jps/jps_debug
var/datum/action/innate/path_debug/sssp/sssp_debug
/datum/pathfind_debug/New(datum/admins/owner)
src.owner = owner
hook_client()
/datum/pathfind_debug/Destroy(force)
QDEL_NULL(jps_debug)
QDEL_NULL(sssp_debug)
return ..()
/datum/pathfind_debug/proc/hook_client()
if(!owner.owner)
return
QDEL_NULL(jps_debug)
QDEL_NULL(sssp_debug)
jps_debug = new
jps_debug.Grant(owner.owner.mob)
sssp_debug = new()
sssp_debug.Grant(owner.owner.mob)
RegisterSignal(owner.owner.mob, COMSIG_MOB_LOGOUT, PROC_REF(on_logout))
/datum/pathfind_debug/proc/on_logout(mob/logging_out)
SIGNAL_HANDLER
UnregisterSignal(logging_out, COMSIG_MOB_LOGOUT)
var/mob/new_mob = owner.owner?.mob
if(!new_mob)
RegisterSignal(logging_out, COMSIG_MOB_LOGIN, PROC_REF(on_login))
return
hook_client()
/datum/pathfind_debug/proc/on_login(mob/logging_in)
SIGNAL_HANDLER
UnregisterSignal(logging_in, list(COMSIG_MOB_LOGOUT, COMSIG_MOB_LOGIN))
hook_client()
/datum/action/innate/path_debug
var/list/image/display_images = list()
/datum/action/innate/path_debug/Activate()
. = ..()
RegisterSignal(owner, COMSIG_MOB_CLICKON, PROC_REF(clicked_somethin))
active = TRUE
/datum/action/innate/path_debug/Deactivate()
UnregisterSignal(owner, COMSIG_MOB_CLICKON)
clear_visuals()
active = FALSE
return ..()
/datum/action/innate/path_debug/proc/clicked_somethin(datum/source, atom/clicked, list/modifiers)
SIGNAL_HANDLER
if(LAZYACCESS(modifiers, SHIFT_CLICK))
return NONE
var/turf/clunked = get_turf(clicked)
if(!clunked)
return NONE
if(LAZYACCESS(modifiers, RIGHT_CLICK))
right_clicked(clunked)
else
left_clicked(clunked)
update_visuals()
if(path_ready())
pathfind()
/datum/action/innate/path_debug/proc/left_clicked(turf/clicked_on)
return
/datum/action/innate/path_debug/proc/right_clicked(turf/clicked_on)
return
/datum/action/innate/path_debug/proc/update_visuals()
clear_visuals()
build_visuals()
owner.client?.images += display_images
/datum/action/innate/path_debug/proc/clear_visuals()
owner.client?.images -= display_images
display_images = list()
/datum/action/innate/path_debug/proc/build_visuals()
return
/datum/action/innate/path_debug/proc/path_ready()
return FALSE
/datum/action/innate/path_debug/proc/pathfind()
INVOKE_ASYNC(src, PROC_REF(run_the_path), GLOB.pathfind_dude)
GLOB.pathfind_dude.moveToNullspace()
/datum/action/innate/path_debug/proc/run_the_path(atom/movable/middle_man)
return
/datum/action/innate/path_debug/proc/render_path(list/turf/draw_list)
if(!length(draw_list))
return list()
var/list/image/turf_images = list()
// Render everything but the first and last
for(var/i in 1 to (length(draw_list) - 1))
var/turf/problem_child = draw_list[i]
var/turf/next = draw_list[i + 1]
turf_images += render_turf(problem_child, get_dir(problem_child, next))
return turf_images
/datum/action/innate/path_debug/proc/render_turf(turf/draw, direction)
var/image/arrow = image('icons/turf/debug.dmi', draw, "arrow", PATH_ARROW_DEBUG_LAYER, direction)
SET_PLANE_EXPLICIT(arrow, BALLOON_CHAT_PLANE, draw)
return arrow
/datum/action/innate/path_debug/jps
name = "JPS Test"
button_icon = 'icons/turf/debug.dmi'
button_icon_state = "jps"
// Mirror vars for jps calls
var/turf/source_turf
var/turf/target_turf
var/max_distance
var/min_distance
var/allowed_on_space
var/turf/blacklisted_turf
var/diagonal_handling
/// List of turfs we are showing to our owner currently
var/list/turf/display_turfs
/datum/action/innate/path_debug/jps/Activate()
. = ..()
max_distance = tgui_input_number(owner, "How far should we be allowed to try and path", "Max Distance", min_value = 1, default = 30)
min_distance = tgui_input_number(owner, "How close should we try and get to the target before stopping", "Min Distance", min_value = 0, default = 0)
allowed_on_space = tgui_alert(owner, "Are we allowed to path over space?", "Space Pathing", buttons = list("Yes", "No")) == "Yes"
var/text_blacklist = tgui_input_text(owner, "Enter any turf path you want to blacklist (You get one)", "Turf Blacklist")
if(text_blacklist)
blacklisted_turf = pick_closest_path(text_blacklist)
else
blacklisted_turf = null
diagonal_handling = DIAGONAL_DO_NOTHING
switch(tgui_input_list(owner, "Pick how you want to handle diagonal moves", "Diagonal Moves", list("Leave Them Be", "Drop All", "Drop Odd Ones")))
if("Leave Them Be")
diagonal_handling = DIAGONAL_DO_NOTHING
if("Drop All")
diagonal_handling = DIAGONAL_REMOVE_ALL
if("Drop Odd Ones")
diagonal_handling = DIAGONAL_REMOVE_CLUNKY
/datum/action/innate/path_debug/jps/Deactivate()
source_turf = null
target_turf = null
display_turfs = list()
return ..()
/datum/action/innate/path_debug/jps/left_clicked(turf/clicked_on)
source_turf = clicked_on
display_turfs = list()
/datum/action/innate/path_debug/jps/right_clicked(turf/clicked_on)
target_turf = clicked_on
display_turfs = list()
/datum/action/innate/path_debug/jps/build_visuals()
. = ..()
if(source_turf)
var/image/start = image('icons/turf/debug.dmi', source_turf, "start", PATH_DEBUG_LAYER)
SET_PLANE_EXPLICIT(start, BALLOON_CHAT_PLANE, source_turf)
display_images += start
if(target_turf)
var/image/end = image('icons/turf/debug.dmi', target_turf, "end", PATH_DEBUG_LAYER)
SET_PLANE_EXPLICIT(end, BALLOON_CHAT_PLANE, target_turf)
display_images += end
display_images += render_path(display_turfs)
/datum/action/innate/path_debug/jps/path_ready()
return (source_turf && target_turf)
/datum/action/innate/path_debug/jps/run_the_path(atom/movable/middle_man)
middle_man.forceMove(source_turf)
display_turfs = get_path_to(middle_man, target_turf, max_distance, min_distance, list(), allowed_on_space, blacklisted_turf, skip_first = FALSE, diagonal_handling = diagonal_handling)
update_visuals()
/datum/action/innate/path_debug/sssp
name = "Pathmap Test"
button_icon = 'icons/turf/debug.dmi'
button_icon_state = "sssp"
// Mirror vars for sssp calls
var/turf/source_turf
var/max_distance
var/allowed_on_space
var/turf/blacklisted_turf
// Turf to display the path to (optional)
var/turf/target_turf
/// List of turfs we are showing to our owner currently
var/datum/path_map/shown_map
/datum/action/innate/path_debug/sssp/Activate()
. = ..()
max_distance = tgui_input_number(owner, "How far should we be allowed to try and path", "Max Distance", min_value = 1, default = 30)
allowed_on_space = tgui_alert(owner, "Are we allowed to path over space?", "Space Pathing", buttons = list("Yes", "No")) == "Yes"
var/text_blacklist = tgui_input_text(owner, "Enter any turf path you want to blacklist (You get one)", "Turf Blacklist")
if(text_blacklist)
blacklisted_turf = pick_closest_path(text_blacklist)
else
blacklisted_turf = null
/datum/action/innate/path_debug/sssp/Deactivate()
source_turf = null
target_turf = null
shown_map = null
return ..()
/datum/action/innate/path_debug/sssp/left_clicked(turf/clicked_on)
source_turf = clicked_on
shown_map = null
/datum/action/innate/path_debug/sssp/right_clicked(turf/clicked_on)
if(clicked_on == target_turf)
target_turf = null
return
target_turf = clicked_on
/datum/action/innate/path_debug/sssp/build_visuals()
. = ..()
if(source_turf)
var/image/start = image('icons/turf/debug.dmi', source_turf, "start", PATH_DEBUG_LAYER)
SET_PLANE_EXPLICIT(start, BALLOON_CHAT_PLANE, source_turf)
display_images += start
if(target_turf)
var/image/end = image('icons/turf/debug.dmi', target_turf, "end", PATH_DEBUG_LAYER)
SET_PLANE_EXPLICIT(end, BALLOON_CHAT_PLANE, target_turf)
display_images += end
if(shown_map)
display_images += render_path(shown_map.get_path_to(target_turf))
else
if(!shown_map)
return
var/list/turf/next_closest = shown_map.next_closest
var/turf/start = shown_map?.start
for(var/turf/next_dude as anything in next_closest)
if(next_dude == start)
continue
display_images += render_turf(next_dude, get_dir(next_dude, next_closest[next_dude]))
/datum/action/innate/path_debug/sssp/path_ready()
return (source_turf && source_turf != shown_map?.start)
/datum/action/innate/path_debug/sssp/run_the_path(atom/movable/middle_man)
middle_man.forceMove(source_turf)
shown_map = get_sssp(middle_man, max_distance, list(), allowed_on_space, blacklisted_turf)
update_visuals()