diff --git a/code/__DEFINES/ai/ai.dm b/code/__DEFINES/ai/ai.dm index e894b32b1f6..d4281a551f8 100644 --- a/code/__DEFINES/ai/ai.dm +++ b/code/__DEFINES/ai/ai.dm @@ -2,17 +2,23 @@ #define GET_TARGETING_STRATEGY(targeting_type) SSai_behaviors.targeting_strategies[targeting_type] #define HAS_AI_CONTROLLER_TYPE(thing, type) istype(thing?.ai_controller, type) + //AI controller flags //If you add a new status, be sure to add it to the ai_controllers subsystem's ai_controllers_by_status list. ///The AI is currently active. #define AI_STATUS_ON "ai_on" ///The AI is currently offline for any reason. #define AI_STATUS_OFF "ai_off" +///The AI is currently in idle mode. +#define AI_STATUS_IDLE "ai_idle" ///For JPS pathing, the maximum length of a path we'll try to generate. Should be modularized depending on what we're doing later on #define AI_MAX_PATH_LENGTH 30 // 30 is possibly overkill since by default we lose interest after 14 tiles of distance, but this gives wiggle room for weaving around obstacles #define AI_BOT_PATH_LENGTH 150 +// How far should we, by default, be looking for interesting things to de-idle? +#define AI_DEFAULT_INTERESTING_DIST 14 + ///Cooldown on planning if planning failed last time #define AI_FAILED_PLANNING_COOLDOWN (1.5 SECONDS) diff --git a/code/controllers/subsystem/ai_controllers.dm b/code/controllers/subsystem/ai_controllers.dm index 44e1948e386..e215f4a4ef7 100644 --- a/code/controllers/subsystem/ai_controllers.dm +++ b/code/controllers/subsystem/ai_controllers.dm @@ -13,9 +13,14 @@ SUBSYSTEM_DEF(ai_controllers) var/list/ai_controllers_by_status = list( AI_STATUS_ON = list(), AI_STATUS_OFF = list(), + AI_STATUS_IDLE = list(), ) ///Assoc List of all AI controllers and the Z level they are on, which we check when someone enters/leaves a Z level to turn them on/off. var/list/ai_controllers_by_zlevel = list() + /// The tick cost of all active AI, calculated on fire. + var/cost_on + /// The tick cost of all idle AI, calculated on fire. + var/cost_idle /datum/controller/subsystem/ai_controllers/Initialize() setup_subtrees() @@ -24,10 +29,20 @@ SUBSYSTEM_DEF(ai_controllers) /datum/controller/subsystem/ai_controllers/stat_entry(msg) var/list/active_list = ai_controllers_by_status[AI_STATUS_ON] var/list/inactive_list = ai_controllers_by_status[AI_STATUS_OFF] - msg = "Active AIs:[length(active_list)]|Inactive:[length(inactive_list)]" + var/list/idle_list = ai_controllers_by_status[AI_STATUS_IDLE] + msg = "Active AIs:[length(active_list)]/[round(cost_on,1)]%|Inactive:[length(inactive_list)]|Idle:[length(idle_list)]/[round(cost_idle,1)]%" return ..() /datum/controller/subsystem/ai_controllers/fire(resumed) + var/timer = TICK_USAGE_REAL + for(var/datum/ai_controller/ai_controller as anything in ai_controllers_by_status[AI_STATUS_IDLE]) + for(var/client/client_found in GLOB.clients) + if(get_dist(get_turf(client_found.mob), get_turf(ai_controller.pawn)) <= ai_controller.interesting_dist) + ai_controller.set_ai_status(AI_STATUS_ON) + break + cost_idle = MC_AVERAGE(cost_idle, TICK_DELTA_TO_MS(TICK_USAGE_REAL - timer)) + + timer = TICK_USAGE_REAL for(var/datum/ai_controller/ai_controller as anything in ai_controllers_by_status[AI_STATUS_ON]) if(!COOLDOWN_FINISHED(ai_controller, failed_planning_cooldown)) continue @@ -37,6 +52,15 @@ SUBSYSTEM_DEF(ai_controllers) ai_controller.SelectBehaviors(wait * 0.1) if(!LAZYLEN(ai_controller.current_behaviors)) //Still no plan COOLDOWN_START(ai_controller, failed_planning_cooldown, AI_FAILED_PLANNING_COOLDOWN) + if(ai_controller.can_idle) + var/found_interesting = FALSE + for(var/client/client_found in GLOB.clients) + if(get_dist(get_turf(client_found.mob), get_turf(ai_controller.pawn)) <= ai_controller.interesting_dist) + found_interesting = TRUE + break + if(!found_interesting) + ai_controller.set_ai_status(AI_STATUS_IDLE) + cost_on = MC_AVERAGE(cost_on, TICK_DELTA_TO_MS(TICK_USAGE_REAL - timer)) ///Creates all instances of ai_subtrees and assigns them to the ai_subtrees list. /datum/controller/subsystem/ai_controllers/proc/setup_subtrees() diff --git a/code/datums/ai/_ai_controller.dm b/code/datums/ai/_ai_controller.dm index 664e89eab09..69d00702202 100644 --- a/code/datums/ai/_ai_controller.dm +++ b/code/datums/ai/_ai_controller.dm @@ -56,6 +56,10 @@ multiple modular subtrees with behaviors // The variables below are fucking stupid and should be put into the blackboard at some point. ///AI paused time var/paused_until = 0 + ///Can this AI idle? + var/can_idle = TRUE + ///What distance should we be checking for interesting things when considering idling/deidling? Defaults to AI_DEFAULT_INTERESTING_DIST + var/interesting_dist = AI_DEFAULT_INTERESTING_DIST /datum/ai_controller/New(atom/new_pawn) change_ai_movement_type(ai_movement) @@ -148,7 +152,7 @@ multiple modular subtrees with behaviors if(ai_traits & CAN_ACT_WHILE_DEAD) return AI_STATUS_ON return AI_STATUS_OFF - + var/turf/pawn_turf = get_turf(mob_pawn) #ifdef TESTING if(!pawn_turf) @@ -175,7 +179,7 @@ multiple modular subtrees with behaviors SSai_controllers.ai_controllers_by_zlevel[new_turf.z] += src var/new_level_clients = SSmobs.clients_by_zlevel[new_turf.z].len if(new_level_clients) - set_ai_status(AI_STATUS_ON) + set_ai_status(AI_STATUS_IDLE) else set_ai_status(AI_STATUS_OFF) @@ -307,7 +311,7 @@ multiple modular subtrees with behaviors /datum/ai_controller/proc/set_ai_status(new_ai_status) if(ai_status == new_ai_status) return FALSE //no change - + //remove old status, if we've got one if(ai_status) SSai_controllers.ai_controllers_by_status[ai_status] -= src @@ -319,6 +323,9 @@ multiple modular subtrees with behaviors if(AI_STATUS_OFF) STOP_PROCESSING(SSai_behaviors, src) CancelActions() + if(AI_STATUS_IDLE) + STOP_PROCESSING(SSai_behaviors, src) + CancelActions() /datum/ai_controller/proc/PauseAi(time) paused_until = world.time + time @@ -378,7 +385,7 @@ multiple modular subtrees with behaviors /datum/ai_controller/proc/on_sentience_lost() SIGNAL_HANDLER UnregisterSignal(pawn, COMSIG_MOB_LOGOUT) - set_ai_status(AI_STATUS_ON) //Can't do anything while player is connected + set_ai_status(AI_STATUS_IDLE) //Can't do anything while player is connected RegisterSignal(pawn, COMSIG_MOB_LOGIN, PROC_REF(on_sentience_gained)) // Turn the controller off if the pawn has been qdeleted diff --git a/code/datums/brain_damage/special.dm b/code/datums/brain_damage/special.dm index bfbf2d84d73..5b8666413e3 100644 --- a/code/datums/brain_damage/special.dm +++ b/code/datums/brain_damage/special.dm @@ -514,7 +514,7 @@ owner.grant_language(/datum/language/monkey, UNDERSTOOD_LANGUAGE, TRAUMA_TRAIT) owner.ai_controller.set_blackboard_key(BB_MONKEY_AGGRESSIVE, prob(75)) if(owner.ai_controller.ai_status == AI_STATUS_OFF) - owner.ai_controller.set_ai_status(AI_STATUS_ON) + owner.ai_controller.set_ai_status(AI_STATUS_IDLE) owner.log_message("became controlled by monkey instincts ([owner.ai_controller.blackboard[BB_MONKEY_AGGRESSIVE] ? "aggressive" : "docile"])", LOG_ATTACK, color = "orange") to_chat(owner, span_warning("You feel the urge to act on your primal instincts...")) // extend original timer if we roll the effect while it's already ongoing diff --git a/code/modules/mob/living/basic/bots/bot_ai.dm b/code/modules/mob/living/basic/bots/bot_ai.dm index 706a8b58ba0..36600f244e7 100644 --- a/code/modules/mob/living/basic/bots/bot_ai.dm +++ b/code/modules/mob/living/basic/bots/bot_ai.dm @@ -27,6 +27,7 @@ var/current_pathing_attempts = 0 ///if we cant reach it after this many attempts, add it to our ignore list var/max_pathing_attempts = 25 + can_idle = FALSE // we want these to be running always /datum/ai_controller/basic_controller/bot/TryPossessPawn(atom/new_pawn) . = ..() diff --git a/code/modules/mob/living/basic/lavaland/mook/mook_ai.dm b/code/modules/mob/living/basic/lavaland/mook/mook_ai.dm index cf79eb06aa6..50416693061 100644 --- a/code/modules/mob/living/basic/lavaland/mook/mook_ai.dm +++ b/code/modules/mob/living/basic/lavaland/mook/mook_ai.dm @@ -26,6 +26,7 @@ GLOBAL_LIST_INIT(mook_commands, list( /datum/ai_planning_subtree/mine_walls/mook, /datum/ai_planning_subtree/wander_away_from_village, ) + can_idle = FALSE // these guys are intended to operate even if nobody's around ///check for faction if not a ash walker, otherwise just attack /datum/targeting_strategy/basic/mook/faction_check(datum/ai_controller/controller, mob/living/living_mob, mob/living/the_target) diff --git a/code/modules/mob/living/basic/ruin_defender/flesh.dm b/code/modules/mob/living/basic/ruin_defender/flesh.dm index 17d625cb591..a80d6847a8a 100644 --- a/code/modules/mob/living/basic/ruin_defender/flesh.dm +++ b/code/modules/mob/living/basic/ruin_defender/flesh.dm @@ -173,7 +173,6 @@ /mob/living/basic/living_limb_flesh/proc/wake_up(atom/limb) visible_message(span_warning("[src] begins flailing around!")) Shake(6, 6, 0.5 SECONDS) - ai_controller.set_ai_status(AI_STATUS_ON) + ai_controller.set_ai_status(AI_STATUS_IDLE) forceMove(limb.drop_location()) qdel(limb) - diff --git a/code/modules/unit_tests/mouse_bite_cable.dm b/code/modules/unit_tests/mouse_bite_cable.dm index c2277fe4fd7..df77976ce47 100644 --- a/code/modules/unit_tests/mouse_bite_cable.dm +++ b/code/modules/unit_tests/mouse_bite_cable.dm @@ -21,6 +21,7 @@ var/fake_dt = SSai_controllers.wait * 0.1 // Set AI - AIs by default are off in z-levels with no client, we have to force it on. biter.ai_controller.set_ai_status(AI_STATUS_ON) + biter.ai_controller.can_idle = FALSE // Select behavior - this will queue finding the cable biter.ai_controller.SelectBehaviors(fake_dt) // Process behavior - this will execute the "locate the cable" behavior