Files
Bubberstation/code/modules/admin/verbs/light_debug.dm
LemonInTheDark 77823ad210 Pathfinding Visualization, JPS fixes, Misc Improvement (#90233)
## About The Pull Request

[cleans up poor namespacing on light debugging
tools](93cc9070d5)

[Implements a pathfinding visualization
tool](ed91f69ac4)

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](87f752e7c3)

[Fixes an infinite loop in pathmaps if we tried to pull a cached path to
an unreachable target, && not ||
4head](10086a655d)

[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?)](f56cc4dd43)

## 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
/🆑
2025-03-28 01:51:57 +02:00

418 lines
14 KiB
Plaintext

/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_lights()
sum[source.source_atom.type] += 1
total += 1
sortTim(sum, associative = TRUE)
var/text = ""
for(var/type in sum)
text += "[type] = [sum[type]]\n"
text += "total iterated: [total]"
for(var/client/lad in GLOB.admins)
var/datum/action/spawn_light/let_there_be = new (lad.mob.mind || lad.mob)
let_there_be.Grant(lad.mob)
// I am sorry
SSdcs.RegisterSignal(SSdcs, COMSIG_GLOB_CLIENT_CONNECT, TYPE_PROC_REF(/datum/controller/subsystem/processing/dcs, on_client_connect))
message_admins(text)
/datum/controller/subsystem/processing/dcs/proc/on_client_connect(datum/source, client/new_lad)
SIGNAL_HANDLER
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_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_lights()
SEND_GLOBAL_SIGNAL(COMSIG_LIGHT_DEBUG_DISABLED)
SSdcs.UnregisterSignal(SSdcs, COMSIG_GLOB_CLIENT_CONNECT)
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_lights()
if(isturf(src) || HAS_TRAIT(src, TRAIT_LIGHTING_DEBUGGED))
return
ADD_TRAIT(src, TRAIT_LIGHTING_DEBUGGED, LIGHT_DEBUG_TRAIT)
GLOB.light_debugged_atoms += WEAKREF(src)
add_filter("debug_light", 0, outline_filter(2, COLOR_CENTCOM_BLUE))
var/static/uid = 0
if(!render_target)
render_target = "light_debug_[uid]"
uid++
var/atom/movable/render_step/color/above_light = new(null, src, "#ffffff23")
SET_PLANE_EXPLICIT(above_light, ABOVE_LIGHTING_PLANE, src)
add_overlay(above_light)
QDEL_NULL(above_light)
// Freeze our light would you please
light_flags |= LIGHT_FROZEN
new /atom/movable/screen/light_button/toggle(src)
new /atom/movable/screen/light_button/edit(src)
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_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)
return
REMOVE_TRAIT(src, TRAIT_LIGHTING_DEBUGGED, LIGHT_DEBUG_TRAIT)
GLOB.light_debugged_atoms -= WEAKREF(src)
remove_filter("debug_light")
// Removes the glow overlay via stupid, sorry
var/atom/movable/render_step/color/above_light = new(null, src, "#ffffff23")
SET_PLANE_EXPLICIT(above_light, ABOVE_LIGHTING_PLANE, src)
cut_overlay(above_light)
QDEL_NULL(above_light)
var/atom/movable/lie_to_areas = src
// Freeze our light would you please
light_flags &= ~LIGHT_FROZEN
for(var/atom/movable/screen/light_button/button in lie_to_areas.vis_contents)
qdel(button)
/atom/movable/screen/light_button
icon = 'icons/testing/lighting_debug.dmi'
plane = BALLOON_CHAT_PLANE // We hijack runechat because we can get multiz niceness without making a new PM
layer = ABOVE_ALL_MOB_LAYER
alpha = 100
var/datum/weakref/last_hovored_ref
/atom/movable/screen/light_button/Initialize(mapload)
. = ..()
attach_to(loc)
/atom/movable/screen/light_button/proc/attach_to(atom/new_owner)
if(loc)
UnregisterSignal(loc, COMSIG_QDELETING)
var/atom/movable/mislead_areas = loc
mislead_areas.vis_contents -= src
forceMove(new_owner)
layer = loc.layer
RegisterSignal(loc, COMSIG_QDELETING, PROC_REF(delete_self))
var/atom/movable/lie_to_areas = loc
lie_to_areas.vis_contents += src
/atom/movable/screen/light_button/proc/delete_self(datum/source)
SIGNAL_HANDLER
qdel(src)
// Entered and Exited won't fire while you're dragging something, because you're still "holding" it
// Very much byond logic, but I want nice for my highlighting, so we fake it with drag
// Copypasta from action buttons
/atom/movable/screen/light_button/MouseDrag(atom/over_object, src_location, over_location, src_control, over_control, params)
. = ..()
if(IS_WEAKREF_OF(over_object, last_hovored_ref))
return
var/atom/old_object
if(last_hovored_ref)
old_object = last_hovored_ref?.resolve()
else // If there's no current ref, we assume it was us. We also treat this as our "first go" location
old_object = src
if(old_object)
old_object.MouseExited(over_location, over_control, params)
last_hovored_ref = WEAKREF(over_object)
over_object.MouseEntered(over_location, over_control, params)
/atom/movable/screen/light_button/mouse_drop_dragged(atom/over, mob/user, src_location, over_location, params)
last_hovored_ref = null
/atom/movable/screen/light_button/MouseEntered(location, control, params)
. = ..()
animate(src, alpha = 255, time = 2)
/atom/movable/screen/light_button/MouseExited(location, control, params)
. = ..()
animate(src, alpha = initial(alpha), time = 2)
/atom/movable/screen/light_button/toggle
name = "Toggle Light"
desc = "Click to turn the light on/off"
icon_state = "light_enable"
/atom/movable/screen/light_button/toggle/attach_to(atom/new_owner)
if(loc)
UnregisterSignal(loc, COMSIG_ATOM_UPDATE_LIGHT_ON)
. = ..()
RegisterSignal(loc, COMSIG_ATOM_UPDATE_LIGHT_ON, PROC_REF(on_changed))
update_appearance()
/atom/movable/screen/light_button/toggle/Click(location, control, params)
. = ..()
if(!check_rights_for(usr.client, R_DEBUG))
return
var/atom/movable/parent = loc
parent.light_flags &= ~LIGHT_FROZEN
loc.set_light(l_on = !loc.light_on)
parent.light_flags |= LIGHT_FROZEN
/atom/movable/screen/light_button/toggle/proc/on_changed()
SIGNAL_HANDLER
update_appearance()
/atom/movable/screen/light_button/toggle/update_icon_state()
. = ..()
if(loc.light_on)
icon_state = "light_enable"
else
icon_state = "light_disable"
/atom/movable/screen/light_button/edit
name = "Edit Light"
desc = "Click to open an editing menu for the light"
icon_state = "light_focus"
/atom/movable/screen/light_button/edit/attach_to(atom/new_owner)
. = ..()
SStgui.try_update_ui(usr, src, null)
/atom/movable/screen/light_button/edit/Click(location, control, params)
. = ..()
ui_interact(usr)
/atom/movable/screen/light_button/edit/ui_state(mob/user)
return ADMIN_STATE(R_DEBUG)
/atom/movable/screen/light_button/edit/can_interact()
return TRUE
/atom/movable/screen/light_button/edit/ui_interact(mob/user, datum/tgui/ui)
ui = SStgui.try_update_ui(user, src, ui)
if(!ui)
ui = new(user, src, "LightController")
ui.open()
/atom/movable/screen/light_button/edit/ui_assets(mob/user)
return list(get_asset_datum(/datum/asset/spritesheet_batched/lights))
/atom/movable/screen/light_button/edit/ui_data()
var/list/data = list()
var/atom/parent = loc
var/list/light_info = list()
light_info["name"] = full_capitalize(parent.name)
light_info["on"] = parent.light_on
light_info["power"] = parent.light_power
light_info["range"] = parent.light_range
light_info["color"] = parent.light_color
light_info["angle"] = parent.light_angle
data["light_info"] = light_info
data["on"] = parent.light_on
data["direction"] = parent.dir
return data
/atom/movable/screen/light_button/edit/ui_static_data(mob/user)
. = ..()
var/list/data = list()
data["templates"] = list()
data["category_ids"] = list()
for(var/id in GLOB.light_types)
var/datum/light_template/template = GLOB.light_types[id]
var/list/insert = list()
var/list/light_info = list()
light_info["name"] = template.name
light_info["power"] = template.power
light_info["range"] = template.range
light_info["color"] = template.color
light_info["angle"] = template.angle
insert["light_info"] = light_info
insert["description"] = template.desc
insert["id"] = template.id
insert["category"] = template.category
if(!data["category_ids"][template.category])
data["category_ids"][template.category] = list()
data["category_ids"][template.category] += id
data["templates"][template.id] = insert
var/datum/light_template/first_template = GLOB.light_types[GLOB.light_types[1]]
data["default_id"] = first_template.id
data["default_category"] = first_template.category
return data
/atom/movable/screen/light_button/edit/ui_act(action, list/params, datum/tgui/ui, datum/ui_state/state)
. = ..()
if(.)
return
var/atom/parent = loc
parent.light_flags &= ~LIGHT_FROZEN
switch(action)
if("set_on")
parent.set_light(l_on = params["value"])
if("change_color")
var/chosen_color = input(ui.user, "Pick new color", "[parent]", parent.light_color) as color|null
if(chosen_color)
parent.set_light(l_color = chosen_color)
if("set_power")
parent.set_light(l_power = params["value"])
if("set_range")
parent.set_light(l_range = params["value"])
if("set_angle")
// We use dir instead of light dir because anything directional should have its lightdir tied
// And this way we can update the sprite too
parent.set_light(l_angle = params["value"])
if("set_dir")
parent.setDir(params["value"])
if("mirror_template")
var/datum/light_template/template = GLOB.light_types[params["id"]]
var/atom/new_light = template.create(parent.loc, parent.dir)
var/atom/movable/lies_to_children = parent
for(var/atom/movable/screen/light_button/button in lies_to_children.vis_contents)
button.attach_to(new_light)
qdel(parent)
if("isolate")
isolate_light(parent)
parent.light_flags |= LIGHT_FROZEN
return TRUE
/// Hides all the lights around a source temporarially, for the sake of figuring out how bad a light bleeds
/// (Except for turf lights, because they're a part of the "scene" and rarely modified)
/proc/isolate_light(atom/source, delay = 7 SECONDS)
var/list/datum/lighting_corner/interesting_corners = source.light?.effect_str
var/list/atom/sources = list()
for(var/datum/lighting_corner/corner as anything in interesting_corners)
for(var/datum/light_source/target_spotted as anything in corner.affecting)
if(isturf(target_spotted.source_atom))
continue
sources[target_spotted.source_atom] = TRUE
sources -= source // Please don't disable yourself
if(!length(sources))
return
// Now that we have all the lights (and a bit more), let's get rid of em
for(var/atom/light_source as anything in sources)
light_source.light_flags &= ~LIGHT_FROZEN
light_source.set_light(l_on = FALSE)
light_source.light_flags |= LIGHT_FROZEN
// Now we sleep until the lighting system has processed them
var/current_tick = SSlighting.times_fired
UNTIL(SSlighting.times_fired > current_tick || QDELETED(source) || !source.light)
if(QDELETED(source) || !source.light)
repopulate_lights(sources)
return
// And finally, wait the allotted time, and reawake em
addtimer(CALLBACK(GLOBAL_PROC, GLOBAL_PROC_REF(repopulate_lights), sources), delay)
/proc/repopulate_lights(list/atom/sources)
for(var/atom/light_source as anything in sources)
light_source.light_flags &= ~LIGHT_FROZEN
light_source.set_light(l_on = TRUE)
light_source.light_flags |= LIGHT_FROZEN
/atom/movable/screen/light_button/move
name = "Move Light"
desc = "Drag to move the light around"
icon_state = "light_move"
mouse_drag_pointer = 'icons/effects/mouse_pointers/light_drag.dmi'
/atom/movable/screen/light_button/move/mouse_drop_dragged(atom/over_object)
if(!ismovable(loc))
return
var/atom/movable/movable_owner = loc
movable_owner.forceMove(get_turf(over_object))
/datum/action/spawn_light
name = "Spawn Light"
desc = "Create a light from a template"
button_icon = 'icons/mob/actions/actions_construction.dmi'
button_icon_state = "light_spawn"
/datum/action/spawn_light/New(Target)
. = ..()
RegisterSignal(SSdcs, COMSIG_LIGHT_DEBUG_DISABLED, PROC_REF(debug_disabled))
/datum/action/spawn_light/proc/debug_disabled()
SIGNAL_HANDLER
qdel(src)
/datum/action/spawn_light/Grant(mob/grant_to)
. = ..()
RegisterSignal(grant_to.client, COMSIG_CLIENT_MOB_LOGIN, PROC_REF(move_action), override = TRUE)
/datum/action/spawn_light/proc/move_action(client/source, mob/new_mob)
SIGNAL_HANDLER
Grant(new_mob)
/datum/action/spawn_light/Trigger(trigger_flags)
. = ..()
ui_interact(usr)
/datum/action/spawn_light/ui_state(mob/user)
return ADMIN_STATE(R_DEBUG)
/datum/action/spawn_light/ui_interact(mob/user, datum/tgui/ui)
ui = SStgui.try_update_ui(user, src, ui)
if(!ui)
ui = new(user, src, "LightSpawn")
ui.open()
/datum/action/spawn_light/ui_assets(mob/user)
return list(get_asset_datum(/datum/asset/spritesheet_batched/lights))
/datum/action/spawn_light/ui_data()
var/list/data = list()
return data
/datum/action/spawn_light/ui_static_data(mob/user)
. = ..()
var/list/data = list()
data["templates"] = list()
data["category_ids"] = list()
for(var/id in GLOB.light_types)
var/datum/light_template/template = GLOB.light_types[id]
var/list/insert = list()
var/list/light_info = list()
light_info["name"] = template.name
light_info["power"] = template.power
light_info["range"] = template.range
light_info["color"] = template.color
light_info["angle"] = template.angle
insert["light_info"] = light_info
insert["description"] = template.desc
insert["id"] = template.id
insert["category"] = template.category
if(!data["category_ids"][template.category])
data["category_ids"][template.category] = list()
data["category_ids"][template.category] += id
data["templates"][template.id] = insert
var/datum/light_template/first_template = GLOB.light_types[GLOB.light_types[1]]
data["default_id"] = first_template.id
data["default_category"] = first_template.category
return data
/datum/action/spawn_light/ui_act(action, list/params, datum/tgui/ui, datum/ui_state/state)
. = ..()
if(.)
return
switch(action)
if("spawn_template")
var/datum/light_template/template = GLOB.light_types[params["id"]]
template.create(get_turf(owner), params["dir"])
return TRUE