Files
Aurora.3/code/modules/mob/living/bot/cleanbot.dm
2020-09-14 11:00:14 +02:00

388 lines
12 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()
var/list/cleanbot_types // Going to use this to generate a list of types once then cull it out locally, see comments below for more info
/obj/effect/decal/cleanable/var
being_cleaned = 0
tmp/mob/living/bot/cleanbot/clean_marked = 0 // If a cleaning bot has marked the cleanable to be cleaned, to prevent multiples from going to the same one.
/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.
var/obj/effect/decal/cleanable/target
var/list/path = list()
var/list/patrol_path = list()
var/list/ignorelist = list()
var/obj/cleanbot_listener/listener
var/beacon_freq = 1445 // navigation beacon frequency
var/signal_sent = 0
var/closest_dist
var/next_dest
var/next_dest_loc
var/cleaning = FALSE
var/screw_loose = FALSE
var/odd_button = FALSE
var/should_patrol = FALSE
var/cleans_blood = TRUE
var/list/target_types = list()
var/maximum_search_range = 7
/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()
listener = new /obj/cleanbot_listener(src)
listener.cleanbot = src
janitorial_supplies |= src
SSradio.add_object(listener, beacon_freq, filter = RADIO_NAVBEACONS)
/mob/living/bot/cleanbot/Destroy()
path = null
patrol_path = null
target = null
ignorelist = null
QDEL_NULL(listener)
global.janitorial_supplies -= src
return ..()
/mob/living/bot/cleanbot/proc/handle_target()
if(target.clean_marked && target.clean_marked != src)
target = null
path = list()
ignorelist |= target
return
if(get_turf(src) == get_turf(target))
if(!cleaning)
UnarmedAttack(target)
return TRUE
if(!path.len)
path = AStar(get_turf(src), get_turf(target), /turf/proc/CardinalTurfsWithAccess, /turf/proc/Distance, 0, 30, id = botcard)
if(!path)
ignorelist |= target
target.clean_marked = null
target = null
path = list()
return
else
step_to(src, path[1])
path -= path[1]
return TRUE
/mob/living/bot/cleanbot/proc/remove_from_ignore(path)
ignorelist -= path
/mob/living/bot/cleanbot/Life()
..()
if(!on)
ignorelist = list()
return
if(ignorelist.len && 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/remove_from_ignore, gib), 600)
/mob/living/bot/cleanbot/think()
..()
if(!on)
return
if(pulledby) // Don't wiggle if someone pulls you
patrol_path?.Cut()
return
var/found_spot
if(!should_patrol)
return
// This loop will progressively search outwards for /cleanables in view(), gradually to prevent excessively large view() calls when none are needed.
search_for: // We use the label so we can break out of this loop from within the next loop.
// Not breaking out of these loops properly is where the infinite loop was coming from before.
for(var/i = 0, i <= maximum_search_range, i++)
clean_for: // This one isn't really needed in this context, but it's good to have in case we expand later.
for(var/obj/effect/decal/cleanable/D in view(i, src))
if(D.clean_marked && D.clean_marked != src)
continue clean_for
var/mob/living/bot/cleanbot/other_bot = locate() in get_turf(D)
if(other_bot && other_bot.cleaning && other_bot != src)
continue clean_for
if((D in ignorelist))
// If the object has been slated to be ignored we continue the loop.
continue clean_for
if((D.type in target_types))
// A matching /cleanable was found, now we want to A* it and see if we can reach it.
patrol_path = list()
target = D
D.clean_marked = src
found_spot = handle_target()
if(found_spot)
break search_for // If the target location is found and pathed properly, break the search loop.
else
target = null // Otherwise we want to try the next cleanable in view, if any.
D.clean_marked = null
if(!found_spot && !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 = 1
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 = AStar(loc, next_dest_loc, /turf/proc/CardinalTurfsWithAccess, /turf/proc/Distance, 0, 120, id = botcard, exclude = null)
signal_sent = 0
else
if(pulledby) // Don't wiggle if someone pulls you
patrol_path = list()
return
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
INVOKE_ASYNC(src, .proc/do_clean, D, clean_time)
/mob/living/bot/cleanbot/proc/do_clean(var/obj/effect/decal/cleanable/D, var/clean_time)
if(D && do_after(src, clean_time))
if(istype(D.loc, /turf/simulated))
var/turf/simulated/f = loc
f.dirt = 0
if(!D)
return
D.clean_marked = null
if(D == target)
target.being_cleaned = FALSE
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, 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/turn_off()
..()
target = null
path = list()
patrol_path = list()
/mob/living/bot/cleanbot/attack_hand(var/mob/user)
if(!has_ui_access(user) && !emagged)
to_chat(user, SPAN_WARNING("The unit's interface refuses to unlock!"))
return
var/dat = ""
dat += "Status: <A href='?src=\ref[src];operation=start'>[on ? "On" : "Off"]</A><BR>"
dat += "Behaviour controls are [locked ? "locked" : "unlocked"]<BR>"
dat += "Maintenance panel is [open ? "opened" : "closed"]"
if(!locked || issilicon(user))
dat += "<BR>Cleans Blood: <A href='?src=\ref[src];operation=blood'>[cleans_blood ? "Yes" : "No"]</A><BR>"
dat += "<BR>Patrol station: <A href='?src=\ref[src];operation=patrol'>[should_patrol ? "Yes" : "No"]</A><BR>"
if(open && !locked)
dat += "Odd looking screw twiddled: <A href='?src=\ref[src];operation=screw'>[screw_loose ? "Yes" : "No"]</A><BR>"
dat += "Weird button pressed: <A href='?src=\ref[src];operation=odd_button'>[odd_button ? "Yes" : "No"]</A>"
var/datum/browser/bot_win = new(user, "autocleaner", "Automatic Station Cleaner v1.2 Controls")
bot_win.set_content(dat)
bot_win.open()
/mob/living/bot/cleanbot/Topic(href, href_list)
if(..())
return
usr.set_machine(src)
add_fingerprint(usr)
if(!has_ui_access(usr) && !emagged)
to_chat(usr, SPAN_WARNING("Insufficient permissions."))
return
switch(href_list["operation"])
if("start")
if(on)
turn_off()
else
turn_on()
if("blood")
cleans_blood = !cleans_blood
get_targets()
if("patrol")
should_patrol = !should_patrol
patrol_path = list()
if("freq")
var/freq = text2num(input("Select frequency for navigation beacons", "Frequnecy", num2text(beacon_freq / 10))) * 10
if(freq > 0)
beacon_freq = freq
if("screw")
screw_loose = !screw_loose
to_chat(usr, SPAN_NOTICE("You twiddle the screw."))
if("odd_button")
odd_button = !odd_button
to_chat(usr, SPAN_NOTICE("You press the weird button."))
attack_hand(usr)
/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()
// To avoid excessive loops, instead of storing a list of top-level types, we're going to store a list of all cleanables.
// It eats a little more memory, but uses quite a bit less CPU when attempting to do the type check in the cleaning routine.
// We're always going to have more memory to work with than CPU when it comes to BYOND and the extra usage is not much.
// And to avoid calling typesof() a bunch, we're going to generate the list once globally then Copy() to the bot's list and remove blood if needed.
// In my tests with around 50 cleanbots actively cleaning up messes it reduced the CPU usage on average around 10%
if(!cleanbot_types)
// This just generates the global list if it hasn't been done already, quick process.
cleanbot_types = typesof(/obj/effect/decal/cleanable/blood,/obj/effect/decal/cleanable/vomit,\
/obj/effect/decal/cleanable/crayon,/obj/effect/decal/cleanable/liquid_fuel,/obj/effect/decal/cleanable/mucus,/obj/effect/decal/cleanable/dirt)
// I honestly forgot you could pass multiple types to typesof() until I accidentally did it here.
target_types = cleanbot_types.Copy()
if(!cleans_blood)
target_types -= typesof(/obj/effect/decal/cleanable/blood)-typesof(/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/dist = get_dist(cleanbot, signal.source.loc)
memorized[recv] = signal.source.loc
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
return ..()
/* Assembly */
/obj/item/bucket_sensor
name = "proxy bucket"
desc = "It's a bucket. With a sensor attached."
icon = 'icons/obj/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(var/obj/item/O, var/mob/user)
..()
if(istype(O, /obj/item/robot_parts/l_arm) || istype(O, /obj/item/robot_parts/r_arm))
user.drop_from_inventory(O, get_turf(src))
qdel(O)
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(O.ispen())
var/t = sanitizeSafe(input(user, "Enter new robot name", name, created_name), MAX_NAME_LEN)
if(!t)
return
if(!in_range(src, usr) && src.loc != usr)
return
created_name = t