Files
Aurora.3/code/modules/mob/living/bot/cleanbot.dm
T
BatrachophrenoandGitHub 3e8e76b464 Improve usefulness of Custodial Supplies Locator app (#20814)
Currently, the Custodial Supplies Locator app provided X and Y coords of
every single mop, bucket, janicart, and cleanbot on the entire server.

This PR makes it so that these objects will only be registered to the
global list of custodial supplies if they are present on the Horizon on
init, and it will also provide Z coords so you can tell if they're on a
different deck.

The app also has a weird built-in GPS function that tracks the reader's
movements instead of the device's. Instead of fixing this to follow the
device, I just removed GPS feedback text- a Janitor can grab one of the
GPS units that comes in their lockers, and this also allows them to be
tracked by other units.
2025-06-22 18:09:07 +00:00

516 lines
15 KiB
Plaintext

// Updated by Nadrew, bits and pieces taken from Baycode, but fairly heavily modified to function here (and because a few bits of the baycode was ehh)
// The main issue in the old code was the Life() loop and the fact that it could go infinite really easily.
// The fix involved labeling the various loops involved so they could be continued and broken properly.
// It also decreases the amount of calls to AStar() and handle_target()
///A list of *types* that cleanbots will look for
GLOBAL_LIST_INIT_TYPED(cleanbot_types, /obj/effect/decal/cleanable, typesof(/obj/effect/decal/cleanable/blood, /obj/effect/decal/cleanable/vomit, /obj/effect/decal/cleanable/flour, \
/obj/effect/decal/cleanable/crayon, /obj/effect/decal/cleanable/liquid_fuel, /obj/effect/decal/cleanable/mucus, /obj/effect/decal/cleanable/dirt))
/obj/effect/decal/cleanable
var/being_cleaned = FALSE
///A reference to a `/mob/living/bot/cleanbot` that wants to clean this turf, or null
var/datum/weakref/clean_marked = null
/mob/living/bot/cleanbot
name = "Cleanbot"
desc = "A little cleaning robot, consisting of a bucket, a proximity sensor, and a prosthetic arm. It looks excited to clean!"
icon_state = "cleanbot0"
req_one_access = list(ACCESS_JANITOR, ACCESS_ROBOTICS)
botcard_access = list(ACCESS_JANITOR, ACCESS_MAINT_TUNNELS)
locked = FALSE // Start unlocked so roboticist can set them to patrol.
///The target we're going to clean up, an `/obj/effect/decal/cleanable` weakref
var/datum/weakref/cleaning_target = null
var/list/path = list()
var/list/patrol_path = list()
//A list of `/datum/weakref` that resolve to `/obj/effect/decal/cleanable`, those are the objects to ignore
var/list/datum/weakref/ignorelist = list()
var/obj/cleanbot_listener/listener
///Used for patrol pathing, navbeacons have this by default
var/beacon_freq = BEACONS_FREQ
///The time at which a "findbeacon" signal was broadcasted; it's used for us to find the list of available beacons in patrol mode, to know where to go
var/signal_sent = 0
var/closest_dist = 9999
var/next_dest
var/next_dest_loc
var/screw_loose = FALSE
var/odd_button = FALSE
var/should_patrol = FALSE
var/cleans_blood = TRUE
///A list of `/obj/effect/decal/cleanable` *types* that this borg can target for cleaning
var/list/target_types = list()
var/maximum_search_range = 7
///Boolean, if it's cleaning something *right now* and waiting for the timer to say it's done
var/cleaning = FALSE
///The turf we got the last movement failure on, since doors have to be bumped to be opened
var/turf/last_movement_failure_turf
/mob/living/bot/cleanbot/Cross(atom/movable/crossed)
if(crossed)
if(istype(crossed, /mob/living/bot/cleanbot))
return FALSE
return ..()
/mob/living/bot/cleanbot/Initialize()
. = ..()
get_targets()
//Do not start to patrol until you're told to, also save processing
set_patrol_mode(FALSE)
listener = new /obj/cleanbot_listener(src)
listener.cleanbot = src
if(is_station_turf(get_turf(src)))
GLOB.janitorial_supplies |= src
SSradio.add_object(listener, beacon_freq, filter = RADIO_NAVBEACONS)
/mob/living/bot/cleanbot/Destroy()
path = null
patrol_path = null
cleaning_target = null
ignorelist = null
next_dest_loc = null
QDEL_NULL(listener)
SSradio.remove_object(listener, beacon_freq)
if(src in GLOB.janitorial_supplies)
GLOB.janitorial_supplies -= src
return ..()
/mob/living/bot/cleanbot/proc/handle_target()
//Get the actual cleanable decal to target
var/obj/effect/decal/cleanable/cleaning_target_cache = cleaning_target?.resolve()
var/mob/living/bot/cleanbot/turf_targeting_cleanbot = cleaning_target_cache?.clean_marked?.resolve()
//If already marked by another borg, ignore it
if(turf_targeting_cleanbot != src)
cleaning_target = null
path = list()
ignorelist |= cleaning_target
return
//If we are over it, clean it up
if(get_turf(src) == get_turf(cleaning_target_cache))
if(!cleaning)
UnarmedAttack(cleaning_target_cache)
return TRUE
//Try to get a path to the location if you don't have one
if(!length(path))
path = get_path_to(src, cleaning_target_cache, 250, 0, botcard.GetAccess(), diagonal_handling=DIAGONAL_REMOVE_ALL)
//No length means there's no path to reach it, add it to the exclusions
if(!length(path))
ignorelist |= cleaning_target
cleaning_target_cache.clean_marked = null
cleaning_target = null
path = list()
if(length(path) && !cleaning)
var/successfully_moved = step_to(src, path[1])
if(successfully_moved)
path -= path[1]
return TRUE
//Something blocked us, look for a different target, we might come back to this in a while
else
for(var/obj/machinery/door/a_door in path[1])
if(last_movement_failure_turf != path[1])
last_movement_failure_turf = path[1]
return
//This is the second failure, invalidate the target
else
ignorelist |= cleaning_target
cleaning_target_cache.clean_marked = null
cleaning_target = null
path = list()
break
/mob/living/bot/cleanbot/proc/remove_from_ignore(datum/weakref/thing_to_unignore)
ignorelist -= thing_to_unignore
/mob/living/bot/cleanbot/Life(seconds_per_tick, times_fired)
if(!..())
return FALSE
if(!on)
ignorelist = list()
return
if(length(ignorelist) && prob(2))
ignorelist -= pick(ignorelist)
if(client)
return
if(cleaning)
return
if(!screw_loose && !odd_button && prob(2) && world.time > last_emote + 2 MINUTES)
custom_emote(AUDIBLE_MESSAGE, "makes an excited beeping booping sound!")
last_emote = world.time
if(screw_loose && prob(5)) // Make a mess
if(istype(loc, /turf/simulated))
var/turf/simulated/T = loc
T.wet_floor()
if(odd_button && prob(5)) // Make a big mess
visible_message(SPAN_WARNING("Some bloody gibs fall out of [src]..."))
var/obj/effect/decal/cleanable/blood/gibs/gib = new /obj/effect/decal/cleanable/blood/gibs(get_turf(src))
ignorelist += gib
addtimer(CALLBACK(src, PROC_REF(remove_from_ignore), gib), 600)
return TRUE
/mob/living/bot/cleanbot/think()
if(pAI) // no AI if we have a pAI installed
return
..()
if(!on)
return
if(pulledby) // Don't wiggle if someone pulls you
patrol_path?.Cut()
return
//We have a path to a target already, follow it
if(length(path))
handle_target()
//Otherwise, look around for a target, or patrol
else
//If we could a spot to clean or not
var/found_spot
for(var/obj/effect/decal/cleanable/D in view(maximum_search_range, src))
var/datum/weakref/cleanable_weakref = WEAKREF(D)
var/mob/living/bot/cleanbot/turf_targeting_cleanbot = D.clean_marked?.resolve()
//Someone already wants this cleanable and it's not us, keep looking
if(!isnull(turf_targeting_cleanbot) && turf_targeting_cleanbot != src)
continue
var/mob/living/bot/cleanbot/other_bot = locate() in get_turf(D)
if(other_bot && other_bot.cleaning && other_bot != src)
continue
// If the object has been slated to be ignored we continue the loop.
if((cleanable_weakref in ignorelist))
continue
// A matching /cleanable was found, now we want to path trace to it and see if we can reach it.
if((D.type in target_types))
patrol_path = list()
cleaning_target = cleanable_weakref
D.clean_marked = WEAKREF(src)
found_spot = handle_target()
if(found_spot)
break // If the target location is found and pathed properly, break the search loop.
else
cleaning_target = null // Otherwise we want to try the next cleanable in view, if any.
D.clean_marked = null
if(!found_spot && !cleaning_target) // No targets in range
if(!patrol_path || !patrol_path.len)
if(!signal_sent || signal_sent > world.time + 200) // Waited enough or didn't send yet
var/datum/radio_frequency/frequency = SSradio.return_frequency(beacon_freq)
if(!frequency)
return
closest_dist = 9999
next_dest = null
next_dest_loc = null
var/datum/signal/signal = new()
signal.source = src
signal.transmission_method = TRANSMISSION_RADIO
signal.data = list("findbeacon" = "patrol")
frequency.post_signal(src, signal, filter = RADIO_NAVBEACONS)
signal_sent = world.time
else
if(next_dest)
next_dest_loc = listener.memorized[next_dest]
if(next_dest_loc)
patrol_path = get_path_to(loc, next_dest_loc, 120, 0, botcard.GetAccess(), diagonal_handling=DIAGONAL_REMOVE_ALL)
signal_sent = 0
else if(should_patrol)
if(patrol_path[1] == loc)
patrol_path -= patrol_path[1]
return
var/moved = step_towards(src, patrol_path[1])
if(moved)
patrol_path -= patrol_path[1]
/mob/living/bot/cleanbot/UnarmedAttack(var/obj/effect/decal/cleanable/D, var/proximity)
. = ..()
if(!.)
return
if(isturf(D))
D = locate(/obj/effect/decal/cleanable) in D
if(!istype(D))
return
if(!src.Adjacent(D))
return
cleaning = TRUE
D.being_cleaned = TRUE
update_icon()
var/clean_time = istype(D, /obj/effect/decal/cleanable/dirt) ? 10 : 50
addtimer(CALLBACK(src, PROC_REF(do_clean), D), clean_time)
/mob/living/bot/cleanbot/proc/do_clean(var/obj/effect/decal/cleanable/D)
if(D)
if(istype(D.loc, /turf/simulated))
var/turf/simulated/f = loc
f.dirt = 0
if(!D)
return
D.clean_marked = null
D.being_cleaned = FALSE
cleaning_target = null
qdel(D)
cleaning = FALSE
update_icon()
/mob/living/bot/cleanbot/explode()
on = FALSE // the first thing i do when i explode is turn off, tbh - geeves
visible_message(SPAN_WARNING("[src] blows apart!"))
var/turf/T = get_turf(src)
new /obj/item/reagent_containers/glass/bucket(T)
new /obj/item/device/assembly/prox_sensor(T)
if(prob(50))
new /obj/item/robot_parts/l_arm(T)
spark(src, 3, GLOB.alldirs)
qdel(src)
return
/mob/living/bot/cleanbot/update_icon()
if(cleaning)
icon_state = "cleanbot-c"
else
icon_state = "cleanbot[on]"
/mob/living/bot/cleanbot/attack_hand(mob/user)
ui_interact(user)
/mob/living/bot/cleanbot/ui_interact(mob/user, datum/tgui/ui)
ui = SStgui.try_update_ui(user, src, ui)
if(!ui)
ui = new(user, src, "CleanBot")
ui.open()
/mob/living/bot/cleanbot/ui_data(mob/user)
var/list/data = list()
data["status"] = on
data["locked"] = locked
data["maintenance_panel"] = open
data["cleans_blood"] = cleans_blood
data["should_patrol"] = should_patrol
data["screw_loose"] = screw_loose
data["odd_button"] = odd_button
data["beacon_freq"] = beacon_freq
return data
/mob/living/bot/cleanbot/ui_static_data(mob/user)
var/list/data = list()
var/list/cleanables_names = list()
for(var/obj/effect/decal/cleanable/cleanable_type as anything in target_types)
cleanables_names |= capitalize_first_letters(initial(cleanable_type.name))
data["cleanable_types"] = cleanables_names
return data
/mob/living/bot/cleanbot/ui_act(action, list/params, datum/tgui/ui, datum/ui_state/state)
. = ..()
if(.)
return
switch(action)
if("toggle_status")
if(on)
turn_off()
else
turn_on()
if("toggle_cleans_blood")
cleans_blood = !cleans_blood
get_targets()
if("toggle_patrol_mode")
set_patrol_mode(!should_patrol)
if("set_frequency")
var/new_frequency = tgui_input_number(usr, "Select frequency for navigation beacons", "Frequnecy", (beacon_freq/10), round_value = FALSE)
if(new_frequency > 0)
SSradio.remove_object(listener, beacon_freq)
beacon_freq = new_frequency*10
SSradio.add_object(listener, beacon_freq, RADIO_NAVBEACONS)
if("toggle_screw")
screw_loose = !screw_loose
to_chat(usr, SPAN_NOTICE("You twiddle the screw."))
if("toggle_odd_button")
odd_button = !odd_button
to_chat(usr, SPAN_NOTICE("You press the weird button."))
/mob/living/bot/cleanbot/Topic(href, href_list)
if(..())
return
add_fingerprint(usr)
attack_hand(usr)
/mob/living/bot/cleanbot/attackby(obj/item/attacking_item, mob/user)
. = ..()
//To refresh the lock/unlock from ID hitting etc.
SStgui.try_update_ui(user, src)
/mob/living/bot/cleanbot/turn_on()
. = ..()
MOB_START_THINKING(src)
/mob/living/bot/cleanbot/turn_off()
. = ..()
cleaning_target = null
path = list()
patrol_path = list()
ignorelist = list()
MOB_STOP_THINKING(src)
/**
* Handles the turn on / off of patrol mode
*
* * state - A Boolean, `TRUE` to turn patrol mode on, `FALSE` to turn it off
*/
/mob/living/bot/cleanbot/proc/set_patrol_mode(state)
closest_dist = initial(closest_dist)
patrol_path.Cut()
if(state)
should_patrol = TRUE
patrol_path = list()
signal_sent = 0
else
should_patrol = FALSE
/mob/living/bot/cleanbot/emag_act(var/remaining_uses, var/mob/user)
. = ..()
if(!screw_loose || !odd_button)
if(user)
to_chat(user, SPAN_NOTICE("The [src] buzzes and beeps."))
odd_button = TRUE
screw_loose = TRUE
return TRUE
/mob/living/bot/cleanbot/proc/get_targets()
target_types = GLOB.cleanbot_types
if(!cleans_blood)
//Well now there's a point, this doesn't clean blood or oil
target_types = target_types.Copy()
target_types -= typesof(/obj/effect/decal/cleanable/blood, /obj/effect/decal/cleanable/blood/oil)
/* Radio object that listens to signals */
/obj/cleanbot_listener
var/mob/living/bot/cleanbot/cleanbot = null
var/list/memorized = list()
/obj/cleanbot_listener/receive_signal(var/datum/signal/signal)
var/recv = signal.data["beacon"]
var/valid = signal.data["patrol"]
if(!recv || !valid || !cleanbot)
return
var/turf/signal_sender_turf = get_turf(signal.source)
var/turf/our_cleanbot_turf = get_turf(cleanbot)
//Nullspace, some other shit, either way, abort
if(!istype(signal_sender_turf) || !istype(our_cleanbot_turf))
return
//At the moment, the path system does not understand zlevels, so this only works on the same zlevel, deal with it, then you can just turn this into:
// if(!AreConnectedZLevels(signal.source.loc?.z, cleanbot.loc?.z))
if(signal_sender_turf.z != our_cleanbot_turf.z)
return
var/dist = get_dist(cleanbot, signal_sender_turf)
memorized[recv] = signal_sender_turf
if(dist < cleanbot.closest_dist) // We check all signals, choosing the closest beacon; then we move to the NEXT one after the closest one
cleanbot.closest_dist = dist
cleanbot.next_dest = signal.data["next_patrol"]
/obj/cleanbot_listener/Destroy()
cleanbot = null
SSradio.remove_object_all(src)
return ..()
/* Assembly */
/obj/item/bucket_sensor
name = "proxy bucket"
desc = "It's a bucket. With a sensor attached."
icon = 'icons/mob/npc/aibots.dmi'
icon_state = "bucket_proxy"
force = 3
throwforce = 10
throw_speed = 2
throw_range = 5
var/created_name = "Cleanbot"
/obj/item/bucket_sensor/attackby(obj/item/attacking_item, mob/user)
..()
if(istype(attacking_item, /obj/item/robot_parts/l_arm) || istype(attacking_item, /obj/item/robot_parts/r_arm))
user.drop_from_inventory(attacking_item, get_turf(src))
qdel(attacking_item)
var/turf/T = get_turf(src)
var/mob/living/bot/cleanbot/A = new /mob/living/bot/cleanbot(T)
A.name = created_name
to_chat(user, SPAN_NOTICE("You add the robot arm to the bucket and sensor assembly. Beep boop!"))
qdel(src)
else if(attacking_item.ispen())
var/t = sanitizeSafe( tgui_input_text(user, "Enter new robot name", name, created_name, MAX_NAME_LEN), MAX_NAME_LEN )
if(!t)
return
if(!in_range(src, usr) && src.loc != usr)
return
created_name = t