mirror of
https://github.com/ParadiseSS13/Paradise.git
synced 2026-08-23 12:07:27 +01:00
wire up ai idling to spatial grid (#30711)
* wire up ai idling to spatial grid * lint * Apply suggestions from code review Co-authored-by: DGamerL <108773801+DGamerL@users.noreply.github.com> Signed-off-by: warriorstar-orion <orion@snowfrost.garden> * stupid loadbearing src * some spatial grid cleanup * make sure to remove controllers in the GLOB * immediately set targets and state for hiveloord brood * properly fail behaviors when movement targets are lost * Apply suggestions from code review Comment formatting Co-authored-by: PollardTheDragon <144391971+PollardTheDragon@users.noreply.github.com> Signed-off-by: Burzah <116982774+Burzah@users.noreply.github.com> * Update code/datums/ai/ai_controller.dm Co-authored-by: PollardTheDragon <144391971+PollardTheDragon@users.noreply.github.com> Signed-off-by: Burzah <116982774+Burzah@users.noreply.github.com> * Update code/datums/ai/ai_controller.dm Co-authored-by: PollardTheDragon <144391971+PollardTheDragon@users.noreply.github.com> Signed-off-by: Burzah <116982774+Burzah@users.noreply.github.com> * Apply suggestions from code review Comment formatting Co-authored-by: PollardTheDragon <144391971+PollardTheDragon@users.noreply.github.com> Signed-off-by: Burzah <116982774+Burzah@users.noreply.github.com> * Update code/datums/ai/ai_controller.dm Co-authored-by: PollardTheDragon <144391971+PollardTheDragon@users.noreply.github.com> Signed-off-by: Burzah <116982774+Burzah@users.noreply.github.com> --------- Signed-off-by: warriorstar-orion <orion@snowfrost.garden> Signed-off-by: Burzah <116982774+Burzah@users.noreply.github.com> Co-authored-by: DGamerL <108773801+DGamerL@users.noreply.github.com> Co-authored-by: Burzah <116982774+Burzah@users.noreply.github.com> Co-authored-by: PollardTheDragon <144391971+PollardTheDragon@users.noreply.github.com>
This commit is contained in:
co-authored by
DGamerL
PollardTheDragon
Burzah
parent
9e87289bd8
commit
be48d7598f
@@ -104,3 +104,21 @@
|
||||
}\
|
||||
/datum/controller/subsystem/movement/##X/fire() {..() /*just so it shows up on the profiler*/} \
|
||||
/datum/controller/subsystem/movement/##X
|
||||
|
||||
#define AI_CONTROLLER_SUBSYSTEM_DEF(X) GLOBAL_REAL(SS##X, /datum/controller/subsystem/ai_controllers/##X);\
|
||||
/datum/controller/subsystem/ai_controllers/##X/New(){\
|
||||
NEW_SS_GLOBAL(SS##X);\
|
||||
PreInit();\
|
||||
ss_id="ai_controller_[#X]";\
|
||||
}\
|
||||
/datum/controller/subsystem/ai_controllers/##X/fire() {..() /*just so it shows up on the profiler*/} \
|
||||
/datum/controller/subsystem/ai_controllers/##X
|
||||
|
||||
#define UNPLANNED_CONTROLLER_SUBSYSTEM_DEF(X) GLOBAL_REAL(SS##X, /datum/controller/subsystem/unplanned_controllers/##X);\
|
||||
/datum/controller/subsystem/unplanned_controllers/##X/New(){\
|
||||
NEW_SS_GLOBAL(SS##X);\
|
||||
PreInit();\
|
||||
ss_id="unplanned_controller_[#X]";\
|
||||
}\
|
||||
/datum/controller/subsystem/unplanned_controllers/##X/fire() {..() /*just so it shows up on the profiler*/} \
|
||||
/datum/controller/subsystem/unplanned_controllers/##X
|
||||
|
||||
@@ -18,3 +18,5 @@
|
||||
#define COMSIG_AI_CONTROLLER_POSSESSED_PAWN "ai_controller_possessed_pawn"
|
||||
///sent from ai controllers when they pick behaviors: (list/datum/ai_behavior/old_behaviors, list/datum/ai_behavior/new_behaviors)
|
||||
#define COMSIG_AI_CONTROLLER_PICKED_BEHAVIORS "ai_controller_picked_behaviors"
|
||||
///sent from ai controllers when they stop possessing a pawn: (datum/ai_controller/source_controller)
|
||||
#define COMSIG_AI_CONTROLLER_UNPOSSESSED_PAWN "ai_controller_unpossessed_pawn"
|
||||
|
||||
@@ -99,6 +99,7 @@
|
||||
#define FIRE_PRIORITY_PING 10
|
||||
#define FIRE_PRIORITY_NIGHTSHIFT 10
|
||||
#define FIRE_PRIORITY_IDLE_NPC 10
|
||||
#define FIRE_PRIORITY_UNPLANNED_NPC 10
|
||||
#define FIRE_PRIORITY_CLEANUP 10
|
||||
#define FIRE_PRIORITY_TICKETS 10
|
||||
#define FIRE_PRIORITY_RESEARCH 10 // SoonTM
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
/// All basic AI subtrees
|
||||
GLOBAL_LIST_EMPTY(ai_subtrees)
|
||||
|
||||
/// Basic AI controllers based on status
|
||||
GLOBAL_LIST_INIT(ai_controllers_by_status, list(
|
||||
AI_STATUS_ON = list(),
|
||||
AI_STATUS_OFF = list(),
|
||||
AI_STATUS_IDLE = list(),
|
||||
))
|
||||
|
||||
/// Basic AI controllers based on their z level
|
||||
GLOBAL_LIST_EMPTY(ai_controllers_by_zlevel)
|
||||
|
||||
/// Basic AI controllers that are currently performing idled behaviors
|
||||
GLOBAL_LIST_INIT_TYPED(unplanned_controllers, /list/datum/ai_controller, list(
|
||||
AI_STATUS_ON = list(),
|
||||
AI_STATUS_IDLE = list(),
|
||||
))
|
||||
@@ -7,59 +7,54 @@ SUBSYSTEM_DEF(ai_controllers)
|
||||
wait = 0.5 SECONDS //Plan every half second if required, not great not terrible.
|
||||
runlevels = RUNLEVEL_GAME | RUNLEVEL_POSTGAME
|
||||
|
||||
///List of all ai_subtree singletons, key is the typepath while assigned value is a newly created instance of the typepath. See setup_subtrees()
|
||||
var/list/datum/ai_planning_subtree/ai_subtrees = list()
|
||||
///Assoc List of all AI statuses and all AI controllers with that status.
|
||||
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()
|
||||
/// Type of status we are interested in running
|
||||
var/planning_status = AI_STATUS_ON
|
||||
/// 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
|
||||
|
||||
var/our_cost
|
||||
/// The tick cost of all currently processed AI, being summed together
|
||||
var/summing_cost
|
||||
|
||||
/datum/controller/subsystem/ai_controllers/Initialize()
|
||||
setup_subtrees()
|
||||
|
||||
/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]
|
||||
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)]%"
|
||||
var/list/planning_list = GLOB.ai_controllers_by_status[planning_status]
|
||||
msg = "Planning AIs:[length(planning_list)]/[round(our_cost,1)]%"
|
||||
return ..()
|
||||
|
||||
/datum/controller/subsystem/ai_controllers/fire(resumed)
|
||||
if(!resumed)
|
||||
summing_cost = 0
|
||||
var/timer = TICK_USAGE_REAL
|
||||
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(!ai_controller.able_to_plan())
|
||||
for(var/datum/ai_controller/ai_controller as anything in GLOB.ai_controllers_by_status[planning_status])
|
||||
if(!ai_controller.able_to_plan)
|
||||
continue
|
||||
ai_controller.select_behaviors(wait / (1 SECONDS))
|
||||
if(!LAZYLEN(ai_controller.current_behaviors)) //Still no plan
|
||||
ai_controller.select_behaviors(wait * 0.1)
|
||||
|
||||
if(!length(ai_controller.current_behaviors)) // Still no plan
|
||||
ai_controller.planning_failed()
|
||||
|
||||
if(MC_TICK_CHECK)
|
||||
break
|
||||
|
||||
cost_on = MC_AVERAGE(cost_on, TICK_DELTA_TO_MS(TICK_USAGE_REAL - timer))
|
||||
summing_cost += TICK_DELTA_TO_MS(TICK_USAGE_REAL - timer)
|
||||
if(MC_TICK_CHECK)
|
||||
return
|
||||
|
||||
///Creates all instances of ai_subtrees and assigns them to the ai_subtrees list.
|
||||
our_cost = MC_AVERAGE(our_cost, summing_cost)
|
||||
|
||||
/// Creates all instances of ai_subtrees and assigns them to the ai_subtrees list.
|
||||
/datum/controller/subsystem/ai_controllers/proc/setup_subtrees()
|
||||
if(length(GLOB.ai_subtrees))
|
||||
return
|
||||
for(var/subtree_type in subtypesof(/datum/ai_planning_subtree))
|
||||
var/datum/ai_planning_subtree/subtree = new subtree_type
|
||||
ai_subtrees[subtree_type] = subtree
|
||||
GLOB.ai_subtrees[subtree_type] = subtree
|
||||
|
||||
///Called when the max Z level was changed, updating our coverage.
|
||||
/// Called when the max Z level was changed, updating our coverage.
|
||||
/datum/controller/subsystem/ai_controllers/proc/on_max_z_changed()
|
||||
if(!islist(ai_controllers_by_zlevel))
|
||||
ai_controllers_by_zlevel = new /list(world.maxz, 0)
|
||||
while(SSai_controllers.ai_controllers_by_zlevel.len < world.maxz)
|
||||
SSai_controllers.ai_controllers_by_zlevel.len++
|
||||
SSai_controllers.ai_controllers_by_zlevel[ai_controllers_by_zlevel.len] = list()
|
||||
if(!length(GLOB.ai_controllers_by_zlevel))
|
||||
GLOB.ai_controllers_by_zlevel = new /list(world.maxz,0)
|
||||
while(GLOB.ai_controllers_by_zlevel.len < world.maxz)
|
||||
GLOB.ai_controllers_by_zlevel.len++
|
||||
GLOB.ai_controllers_by_zlevel[GLOB.ai_controllers_by_zlevel.len] = list()
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
UNPLANNED_CONTROLLER_SUBSYSTEM_DEF(idle_unplanned_controllers)
|
||||
name = "Unplanned AI Idle Controllers"
|
||||
wait = 2.5 SECONDS
|
||||
target_status = AI_STATUS_IDLE
|
||||
@@ -0,0 +1,34 @@
|
||||
GLOBAL_LIST_EMPTY(unplanned_controller_subsystems)
|
||||
/// Handles making mobs perform lightweight "idle" behaviors such as wandering around when they have nothing planned
|
||||
SUBSYSTEM_DEF(unplanned_controllers)
|
||||
name = "AI Unplanned Controllers"
|
||||
flags = SS_POST_FIRE_TIMING|SS_BACKGROUND
|
||||
priority = FIRE_PRIORITY_UNPLANNED_NPC
|
||||
wait = 0.25 SECONDS
|
||||
runlevels = RUNLEVEL_GAME | RUNLEVEL_POSTGAME
|
||||
/// What ai status are we interested in
|
||||
var/target_status = AI_STATUS_ON
|
||||
var/list/current_run = list()
|
||||
|
||||
/datum/controller/subsystem/unplanned_controllers/Initialize()
|
||||
GLOB.unplanned_controller_subsystems += src
|
||||
|
||||
/datum/controller/subsystem/unplanned_controllers/Destroy()
|
||||
GLOB.unplanned_controller_subsystems -= src
|
||||
return ..()
|
||||
|
||||
/datum/controller/subsystem/unplanned_controllers/stat_entry(msg)
|
||||
msg = "Planning AIs:[length(GLOB.unplanned_controllers[target_status])]"
|
||||
return ..()
|
||||
|
||||
/datum/controller/subsystem/unplanned_controllers/fire(resumed)
|
||||
if(!resumed)
|
||||
src.current_run = GLOB.unplanned_controllers[target_status].Copy()
|
||||
var/list/current_run = src.current_run // cache for sonic speed
|
||||
while(length(current_run))
|
||||
var/datum/ai_controller/unplanned = current_run[current_run.len]
|
||||
current_run.len--
|
||||
if(!QDELETED(unplanned))
|
||||
unplanned.idle_behavior.perform_idle_behavior(wait * 0.1, unplanned)
|
||||
if(MC_TICK_CHECK)
|
||||
return
|
||||
@@ -0,0 +1,8 @@
|
||||
AI_CONTROLLER_SUBSYSTEM_DEF(ai_idle_controllers)
|
||||
name = "AI Idle Controllers"
|
||||
flags = SS_POST_FIRE_TIMING | SS_BACKGROUND
|
||||
priority = FIRE_PRIORITY_IDLE_NPC
|
||||
init_order = INIT_ORDER_AI_IDLE_CONTROLLERS
|
||||
wait = 5 SECONDS
|
||||
runlevels = RUNLEVEL_GAME
|
||||
planning_status = AI_STATUS_IDLE
|
||||
@@ -0,0 +1,6 @@
|
||||
PROCESSING_SUBSYSTEM_DEF(idle_ai_behaviors)
|
||||
name = "AI Idle Behaviors"
|
||||
flags = SS_NO_INIT | SS_BACKGROUND
|
||||
wait = 1.5 SECONDS
|
||||
priority = FIRE_PRIORITY_IDLE_NPC
|
||||
init_order = INIT_ORDER_AI_IDLE_CONTROLLERS // must execute only after ai behaviors are initialized
|
||||
@@ -44,7 +44,7 @@
|
||||
/// [AI_BEHAVIOR_FAILED].
|
||||
/datum/ai_behavior/proc/finish_action(datum/ai_controller/controller, succeeded, ...)
|
||||
SHOULD_CALL_PARENT(TRUE)
|
||||
LAZYREMOVE(controller.current_behaviors, src)
|
||||
controller.dequeue_behavior(src)
|
||||
controller.behavior_args -= type
|
||||
// If this was a movement task, reset our movement target if necessary
|
||||
if(!(behavior_flags & AI_BEHAVIOR_REQUIRE_MOVEMENT))
|
||||
|
||||
+210
-84
@@ -26,9 +26,9 @@ RESTRICT_TYPE(/datum/ai_controller)
|
||||
/// Bitfield of traits for this AI to handle extra behavior.
|
||||
var/ai_traits = NONE
|
||||
/// Current actions planned to be performed by the AI in the upcoming plan.
|
||||
var/list/planned_behaviors
|
||||
var/list/planned_behaviors = list()
|
||||
/// Current actions being performed by the AI.
|
||||
var/list/current_behaviors
|
||||
var/list/current_behaviors = list()
|
||||
/// Current actions and their respective last time ran as an assoc list.
|
||||
var/list/behavior_cooldowns = list()
|
||||
/// Current status of AI (OFF/ON)
|
||||
@@ -55,6 +55,8 @@ RESTRICT_TYPE(/datum/ai_controller)
|
||||
|
||||
/// The idle behavior this AI performs when it has no actions.
|
||||
var/datum/idle_behavior/idle_behavior = null
|
||||
/// Our current cell grid.
|
||||
var/datum/cell_tracker/our_cells
|
||||
|
||||
// Movement related things here
|
||||
/// Reference to the movement datum we use. Is a type on initialize but becomes a ref afterwards.
|
||||
@@ -75,10 +77,11 @@ RESTRICT_TYPE(/datum/ai_controller)
|
||||
/// Make sure you hook update_able_to_run() in setup_able_to_run() to whatever parameters changing that you added
|
||||
/// Otherwise we will not pay attention to them changing
|
||||
var/able_to_run = FALSE
|
||||
/// are we even able to plan?
|
||||
var/able_to_plan = TRUE
|
||||
/// are we currently on failed planning timeout?
|
||||
var/on_failed_planning_timeout = FALSE
|
||||
|
||||
|
||||
/datum/ai_controller/New(atom/new_pawn)
|
||||
change_ai_movement_type(ai_movement)
|
||||
init_subtrees()
|
||||
@@ -90,15 +93,19 @@ RESTRICT_TYPE(/datum/ai_controller)
|
||||
possess_pawn(new_pawn)
|
||||
|
||||
/datum/ai_controller/Destroy(force)
|
||||
set_ai_status(AI_STATUS_OFF)
|
||||
unpossess_pawn(FALSE)
|
||||
|
||||
if(ai_status)
|
||||
GLOB.ai_controllers_by_status[ai_status] -= src
|
||||
|
||||
our_cells = null
|
||||
set_movement_target(type, null)
|
||||
if(ai_movement.moving_controllers[src])
|
||||
ai_movement.stop_moving_towards(src)
|
||||
|
||||
LAZYCLEARLIST(planned_behaviors)
|
||||
planned_behaviors.Cut()
|
||||
LAZYCLEARLIST(planning_subtrees)
|
||||
LAZYCLEARLIST(current_behaviors)
|
||||
current_behaviors.Cut()
|
||||
|
||||
return ..()
|
||||
|
||||
@@ -135,7 +142,7 @@ RESTRICT_TYPE(/datum/ai_controller)
|
||||
return
|
||||
var/list/temp_subtree_list = list()
|
||||
for(var/subtree in planning_subtrees)
|
||||
var/subtree_instance = SSai_controllers.ai_subtrees[subtree]
|
||||
var/subtree_instance = GLOB.ai_subtrees[subtree]
|
||||
temp_subtree_list += subtree_instance
|
||||
planning_subtrees = temp_subtree_list
|
||||
|
||||
@@ -162,7 +169,7 @@ RESTRICT_TYPE(/datum/ai_controller)
|
||||
|
||||
var/turf/pawn_turf = get_turf(pawn)
|
||||
if(pawn_turf)
|
||||
SSai_controllers.ai_controllers_by_zlevel[pawn_turf.z] += src
|
||||
GLOB.ai_controllers_by_zlevel[pawn_turf.z] += src
|
||||
|
||||
SEND_SIGNAL(src, COMSIG_AI_CONTROLLER_POSSESSED_PAWN)
|
||||
|
||||
@@ -174,6 +181,18 @@ RESTRICT_TYPE(/datum/ai_controller)
|
||||
update_able_to_run()
|
||||
setup_able_to_run()
|
||||
|
||||
our_cells = new(interesting_dist, interesting_dist, 1)
|
||||
set_new_cells()
|
||||
|
||||
RegisterSignal(pawn, COMSIG_MOVABLE_MOVED, PROC_REF(update_grid))
|
||||
|
||||
/datum/ai_controller/proc/update_grid(datum/source, datum/spatial_grid_cell/new_cell)
|
||||
SIGNAL_HANDLER // COMSIG_MOVABLE_MOVED
|
||||
|
||||
set_new_cells()
|
||||
if(current_movement_target)
|
||||
check_target_max_distance()
|
||||
|
||||
/datum/ai_controller/proc/on_movement_target_move(datum/source)
|
||||
SIGNAL_HANDLER // COMSIG_MOVABLE_MOVED
|
||||
check_target_max_distance()
|
||||
@@ -186,6 +205,66 @@ RESTRICT_TYPE(/datum/ai_controller)
|
||||
if(get_dist(current_movement_target, pawn) > max_target_distance)
|
||||
cancel_actions()
|
||||
|
||||
/datum/ai_controller/proc/set_new_cells()
|
||||
if(isnull(our_cells))
|
||||
return
|
||||
|
||||
var/turf/our_turf = get_turf(pawn)
|
||||
|
||||
if(isnull(our_turf))
|
||||
return
|
||||
|
||||
var/list/cell_collections = our_cells.recalculate_cells(our_turf)
|
||||
|
||||
for(var/datum/old_grid as anything in cell_collections[2])
|
||||
UnregisterSignal(old_grid, list(SPATIAL_GRID_CELL_ENTERED(SPATIAL_GRID_CONTENTS_TYPE_CLIENTS), SPATIAL_GRID_CELL_EXITED(SPATIAL_GRID_CONTENTS_TYPE_CLIENTS)))
|
||||
|
||||
for(var/datum/spatial_grid_cell/new_grid as anything in cell_collections[1])
|
||||
RegisterSignal(new_grid, SPATIAL_GRID_CELL_ENTERED(SPATIAL_GRID_CONTENTS_TYPE_CLIENTS), PROC_REF(on_client_enter))
|
||||
RegisterSignal(new_grid, SPATIAL_GRID_CELL_EXITED(SPATIAL_GRID_CONTENTS_TYPE_CLIENTS), PROC_REF(on_client_exit))
|
||||
|
||||
recalculate_idle()
|
||||
|
||||
/datum/ai_controller/proc/should_idle()
|
||||
if(!can_idle || isnull(our_cells))
|
||||
return FALSE
|
||||
for(var/datum/spatial_grid_cell/grid as anything in our_cells.member_cells)
|
||||
if(locate(/mob/living) in grid.client_contents)
|
||||
return FALSE
|
||||
return TRUE
|
||||
|
||||
/datum/ai_controller/proc/recalculate_idle(datum/exited)
|
||||
if(ai_status == AI_STATUS_OFF)
|
||||
return
|
||||
|
||||
var/distance = INFINITY
|
||||
if(islist(exited))
|
||||
var/list/exited_list = exited
|
||||
distance = get_dist(pawn, exited_list[1])
|
||||
else if(isatom(exited))
|
||||
var/atom/exited_atom = exited
|
||||
distance = get_dist(pawn, exited_atom)
|
||||
|
||||
if(distance <= interesting_dist) // is our target in between interesting cells?
|
||||
return
|
||||
|
||||
if(should_idle())
|
||||
set_ai_status(AI_STATUS_IDLE)
|
||||
|
||||
/datum/ai_controller/proc/on_client_enter(datum/source, list/target_list)
|
||||
SIGNAL_HANDLER // COMSIG_CLIENT_ENTER
|
||||
|
||||
if(!(locate(/mob/living) in target_list))
|
||||
return
|
||||
|
||||
if(ai_status == AI_STATUS_IDLE)
|
||||
set_ai_status(AI_STATUS_ON)
|
||||
|
||||
/datum/ai_controller/proc/on_client_exit(datum/source, datum/exited)
|
||||
SIGNAL_HANDLER // COMSIG_CLIENT_EXIT
|
||||
|
||||
recalculate_idle(exited)
|
||||
|
||||
/// Sets the AI on or off based on current conditions, call to reset after you've manually disabled it somewhere
|
||||
/datum/ai_controller/proc/reset_ai_status()
|
||||
set_ai_status(get_expected_ai_status())
|
||||
@@ -197,20 +276,20 @@ RESTRICT_TYPE(/datum/ai_controller)
|
||||
* Returns AI_STATUS_ON otherwise.
|
||||
*/
|
||||
/datum/ai_controller/proc/get_expected_ai_status()
|
||||
. = AI_STATUS_ON
|
||||
if(isnull(get_turf(pawn)))
|
||||
return AI_STATUS_OFF
|
||||
|
||||
if(!ismob(pawn))
|
||||
return
|
||||
return AI_STATUS_ON
|
||||
|
||||
var/mob/living/mob_pawn = pawn
|
||||
if(!continue_processing_when_client && mob_pawn.client)
|
||||
. = AI_STATUS_OFF
|
||||
|
||||
if(ai_traits & AI_FLAG_CAN_ACT_WHILE_DEAD)
|
||||
return
|
||||
return AI_STATUS_OFF
|
||||
|
||||
if(mob_pawn.stat == DEAD)
|
||||
. = AI_STATUS_OFF
|
||||
if(ai_traits & AI_FLAG_CAN_ACT_WHILE_DEAD)
|
||||
return AI_STATUS_ON
|
||||
return AI_STATUS_OFF
|
||||
|
||||
var/turf/pawn_turf = get_turf(mob_pawn)
|
||||
#ifdef GAME_TESTS
|
||||
@@ -218,7 +297,11 @@ RESTRICT_TYPE(/datum/ai_controller)
|
||||
CRASH("AI controller [src] controlling pawn ([pawn]) is not on a turf.")
|
||||
#endif
|
||||
if(!SSmobs.clients_by_zlevel || !length(SSmobs.clients_by_zlevel[pawn_turf.z]) || on_failed_planning_timeout)
|
||||
. = AI_STATUS_OFF
|
||||
return AI_STATUS_OFF
|
||||
if(should_idle())
|
||||
return AI_STATUS_IDLE
|
||||
|
||||
return AI_STATUS_ON
|
||||
|
||||
/// Called when the AI controller pawn changes z levels.
|
||||
/// We check if there's any clients on the new one and wake up the AI if there is.
|
||||
@@ -230,9 +313,9 @@ RESTRICT_TYPE(/datum/ai_controller)
|
||||
if(mob_pawn?.client && !continue_processing_when_client)
|
||||
return
|
||||
if(old_turf)
|
||||
SSai_controllers.ai_controllers_by_zlevel[old_turf.z] -= src
|
||||
GLOB.ai_controllers_by_zlevel[old_turf.z] -= src
|
||||
if(new_turf)
|
||||
SSai_controllers.ai_controllers_by_zlevel[new_turf.z] += src
|
||||
GLOB.ai_controllers_by_zlevel[new_turf.z] += src
|
||||
reset_ai_status()
|
||||
|
||||
/// Abstract proc for initializing the pawn to the new controller
|
||||
@@ -241,17 +324,20 @@ RESTRICT_TYPE(/datum/ai_controller)
|
||||
|
||||
/// Proc for deinitializing the pawn to the old controller
|
||||
/datum/ai_controller/proc/unpossess_pawn(destroy)
|
||||
SHOULD_CALL_PARENT(TRUE)
|
||||
if(isnull(pawn))
|
||||
return // instantiated without an applicable pawn, fine
|
||||
|
||||
UnregisterSignal(pawn, list(COMSIG_MOB_LOGIN, COMSIG_MOB_LOGOUT, COMSIG_MOB_STATCHANGE, COMSIG_PARENT_QDELETING))
|
||||
SEND_SIGNAL(src, COMSIG_AI_CONTROLLER_UNPOSSESSED_PAWN)
|
||||
set_ai_status(AI_STATUS_OFF)
|
||||
UnregisterSignal(pawn, list(COMSIG_MOVABLE_Z_CHANGED, COMSIG_MOB_LOGIN, COMSIG_MOB_LOGOUT, COMSIG_MOB_STATCHANGE, COMSIG_PARENT_QDELETING))
|
||||
clear_able_to_run()
|
||||
if(ai_movement.moving_controllers[src])
|
||||
ai_movement.stop_moving_towards(src)
|
||||
var/turf/pawn_turf = get_turf(pawn)
|
||||
if(pawn_turf)
|
||||
SSai_controllers.ai_controllers_by_zlevel[pawn_turf.z] -= src
|
||||
if(ai_status)
|
||||
SSai_controllers.ai_controllers_by_status[ai_status] -= src
|
||||
GLOB.ai_controllers_by_zlevel[pawn_turf.z] -= src
|
||||
remove_from_unplanned_controllers()
|
||||
pawn.ai_controller = null
|
||||
pawn = null
|
||||
if(destroy)
|
||||
@@ -315,77 +401,55 @@ RESTRICT_TYPE(/datum/ai_controller)
|
||||
// in the AI controller implementation are actually the managing subsystem's `wait`.
|
||||
seconds_per_tick /= (1 SECONDS)
|
||||
|
||||
if(!able_to_run)
|
||||
GLOB.move_manager.stop_looping(pawn) //stop moving
|
||||
return //this should remove them from processing in the future through event-based stuff.
|
||||
|
||||
if(!LAZYLEN(current_behaviors) && idle_behavior)
|
||||
idle_behavior.perform_idle_behavior(seconds_per_tick, src) //Do some stupid shit while we have nothing to do
|
||||
return
|
||||
|
||||
for(var/datum/ai_behavior/current_behavior as anything in current_behaviors)
|
||||
// Convert the current behaviour action cooldown to realtime seconds from deciseconds.current_behavior
|
||||
// Then pick the max of this and the seconds_per_tick passed to ai_controller.process()
|
||||
// Action cooldowns cannot happen faster than seconds_per_tick, so seconds_per_tick should be the value used in this scenario.
|
||||
var/action_seconds_per_tick = max(current_behavior.get_cooldown(src) * 0.1, seconds_per_tick)
|
||||
|
||||
if(current_behavior.behavior_flags & AI_BEHAVIOR_REQUIRE_MOVEMENT) //Might need to move closer
|
||||
if(isnull(current_movement_target))
|
||||
fail_behavior(current_behavior)
|
||||
return
|
||||
|
||||
// Stops pawns from performing such actions that should require the target to be adjacent.
|
||||
var/atom/movable/moving_pawn = pawn
|
||||
var/can_reach = !(current_behavior.behavior_flags & AI_BEHAVIOR_REQUIRE_REACH) || moving_pawn.can_reach_nested_adjacent(current_movement_target)
|
||||
if(can_reach && current_behavior.required_distance >= get_dist(moving_pawn, current_movement_target)) // Are we close enough to engage?
|
||||
if(ai_movement.moving_controllers[src] == current_movement_target) // We are close enough, if we're moving stop.
|
||||
ai_movement.stop_moving_towards(src)
|
||||
|
||||
if(behavior_cooldowns[current_behavior] > world.time) // Still on cooldown
|
||||
continue
|
||||
process_behavior(action_seconds_per_tick, current_behavior)
|
||||
return
|
||||
|
||||
else if(ai_movement.moving_controllers[src] != current_movement_target) // We're too far, if we're not already moving start doing it.
|
||||
ai_movement.start_moving_towards(src, current_movement_target, current_behavior.required_distance) // Then start moving
|
||||
|
||||
if(current_behavior.behavior_flags & AI_BEHAVIOR_MOVE_AND_PERFORM) // If we can move and perform then do so.
|
||||
if(behavior_cooldowns[current_behavior] > world.time) // Still on cooldown
|
||||
continue
|
||||
process_behavior(action_seconds_per_tick, current_behavior)
|
||||
return
|
||||
else // No movement required
|
||||
if(!(current_behavior.behavior_flags & AI_BEHAVIOR_REQUIRE_MOVEMENT))
|
||||
if(behavior_cooldowns[current_behavior] > world.time) // Still on cooldown
|
||||
continue
|
||||
process_behavior(action_seconds_per_tick, current_behavior)
|
||||
return
|
||||
|
||||
/// Determines whether the AI can currently make a new plan.
|
||||
/datum/ai_controller/proc/able_to_plan()
|
||||
. = TRUE
|
||||
if(QDELETED(pawn))
|
||||
return FALSE
|
||||
for(var/datum/ai_behavior/current_behavior as anything in current_behaviors)
|
||||
if(!(current_behavior.behavior_flags & AI_BEHAVIOR_CAN_PLAN_DURING_EXECUTION)) // We have a behavior that blocks planning
|
||||
return FALSE
|
||||
if(isnull(current_movement_target))
|
||||
fail_behavior(current_behavior)
|
||||
return
|
||||
|
||||
/// Stops pawns from performing such actions that should require the target to be adjacent.
|
||||
var/atom/movable/moving_pawn = pawn
|
||||
var/can_reach = !(current_behavior.behavior_flags & AI_BEHAVIOR_REQUIRE_REACH) || moving_pawn.can_reach(current_movement_target)
|
||||
if(can_reach && current_behavior.required_distance >= get_dist(moving_pawn, current_movement_target)) // Are we close enough to engage?
|
||||
if(ai_movement.moving_controllers[src] == current_movement_target) // We are close enough, if we're moving stop.
|
||||
ai_movement.stop_moving_towards(src)
|
||||
|
||||
if(behavior_cooldowns[current_behavior] > world.time) // Still on cooldown
|
||||
continue
|
||||
process_behavior(action_seconds_per_tick, current_behavior)
|
||||
return
|
||||
|
||||
if(ai_movement.moving_controllers[src] != current_movement_target) // We're too far, if we're not already moving start doing it.
|
||||
ai_movement.start_moving_towards(src, current_movement_target, current_behavior.required_distance) // Then start moving
|
||||
|
||||
if(current_behavior.behavior_flags & AI_BEHAVIOR_MOVE_AND_PERFORM) // If we can move and perform then do so.
|
||||
if(behavior_cooldowns[current_behavior] > world.time) // Still on cooldown
|
||||
continue
|
||||
process_behavior(action_seconds_per_tick, current_behavior)
|
||||
return
|
||||
|
||||
/// This is where you decide what actions are taken by the AI.
|
||||
/datum/ai_controller/proc/select_behaviors(seconds_per_tick)
|
||||
SHOULD_NOT_SLEEP(TRUE)
|
||||
|
||||
if(!COOLDOWN_FINISHED(src, failed_planning_cooldown))
|
||||
return FALSE
|
||||
planned_behaviors.Cut()
|
||||
|
||||
LAZYINITLIST(current_behaviors)
|
||||
LAZYCLEARLIST(planned_behaviors)
|
||||
|
||||
if(LAZYLEN(planning_subtrees))
|
||||
for(var/datum/ai_planning_subtree/subtree as anything in planning_subtrees)
|
||||
if(subtree.select_behaviors(src, seconds_per_tick) == SUBTREE_RETURN_FINISH_PLANNING)
|
||||
break
|
||||
for(var/datum/ai_planning_subtree/subtree as anything in planning_subtrees)
|
||||
if(subtree.select_behaviors(src, seconds_per_tick) == SUBTREE_RETURN_FINISH_PLANNING)
|
||||
break
|
||||
|
||||
for(var/datum/ai_behavior/current_behavior as anything in current_behaviors)
|
||||
if(LAZYACCESS(planned_behaviors, current_behavior))
|
||||
if(current_behavior in planned_behaviors)
|
||||
continue
|
||||
var/list/arguments = list(src, FALSE)
|
||||
var/list/stored_arguments = behavior_args[type]
|
||||
@@ -394,24 +458,57 @@ RESTRICT_TYPE(/datum/ai_controller)
|
||||
current_behavior.finish_action(arglist(arguments))
|
||||
|
||||
/// This proc handles changing AI status, and starts/stops processing if required.
|
||||
/datum/ai_controller/proc/set_ai_status(new_ai_status)
|
||||
/datum/ai_controller/proc/set_ai_status(new_ai_status, additional_flags = NONE)
|
||||
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
|
||||
GLOB.ai_controllers_by_status[ai_status] -= src
|
||||
remove_from_unplanned_controllers()
|
||||
stop_previous_processing()
|
||||
ai_status = new_ai_status
|
||||
SSai_controllers.ai_controllers_by_status[new_ai_status] += src
|
||||
GLOB.ai_controllers_by_status[new_ai_status] += src
|
||||
if(ai_status == AI_STATUS_OFF)
|
||||
if(!(additional_flags & AI_PREVENT_CANCEL_ACTIONS))
|
||||
cancel_actions()
|
||||
return
|
||||
if(!length(current_behaviors))
|
||||
add_to_unplanned_controllers()
|
||||
return
|
||||
start_ai_processing()
|
||||
|
||||
/datum/ai_controller/proc/start_ai_processing()
|
||||
switch(ai_status)
|
||||
if(AI_STATUS_ON)
|
||||
START_PROCESSING(SSai_behaviors, src)
|
||||
if(AI_STATUS_OFF, AI_STATUS_IDLE)
|
||||
if(AI_STATUS_IDLE)
|
||||
START_PROCESSING(SSidle_ai_behaviors, src)
|
||||
|
||||
/datum/ai_controller/proc/stop_previous_processing()
|
||||
switch(ai_status)
|
||||
if(AI_STATUS_ON)
|
||||
STOP_PROCESSING(SSai_behaviors, src)
|
||||
cancel_actions()
|
||||
if(AI_STATUS_IDLE)
|
||||
STOP_PROCESSING(SSidle_ai_behaviors, src)
|
||||
|
||||
/datum/ai_controller/proc/pause_ai(time)
|
||||
paused_until = world.time + time
|
||||
update_able_to_run()
|
||||
addtimer(CALLBACK(src, PROC_REF(update_able_to_run)), time)
|
||||
|
||||
/datum/ai_controller/proc/add_to_unplanned_controllers()
|
||||
if(isnull(ai_status) || ai_status == AI_STATUS_OFF || isnull(idle_behavior))
|
||||
return
|
||||
GLOB.unplanned_controllers[ai_status][src] = TRUE
|
||||
|
||||
/datum/ai_controller/proc/remove_from_unplanned_controllers()
|
||||
if(isnull(ai_status) || ai_status == AI_STATUS_OFF)
|
||||
return
|
||||
GLOB.unplanned_controllers[ai_status] -= src
|
||||
for(var/datum/controller/subsystem/unplanned_controllers/potential_holder as anything in GLOB.unplanned_controller_subsystems)
|
||||
if(potential_holder.target_status == ai_status)
|
||||
potential_holder.current_run -= src
|
||||
|
||||
/datum/ai_controller/proc/modify_cooldown(datum/ai_behavior/behavior, new_cooldown)
|
||||
behavior_cooldowns[behavior] = new_cooldown
|
||||
@@ -426,21 +523,50 @@ RESTRICT_TYPE(/datum/ai_controller)
|
||||
|
||||
// It's still in the plan, don't add it again to current_behaviors
|
||||
// but do keep it in the planned behavior list so its not cancelled
|
||||
if(LAZYACCESS(current_behaviors, behavior))
|
||||
LAZYADDASSOC(planned_behaviors, behavior, TRUE)
|
||||
if(current_behaviors[behavior])
|
||||
planned_behaviors[behavior] = TRUE
|
||||
return
|
||||
|
||||
if(!behavior.setup(arglist(arguments)))
|
||||
return
|
||||
LAZYADDASSOC(current_behaviors, behavior, TRUE)
|
||||
LAZYADDASSOC(planned_behaviors, behavior, TRUE)
|
||||
|
||||
var/should_exit_unplanned = !length(current_behaviors)
|
||||
planned_behaviors[behavior] = TRUE
|
||||
current_behaviors[behavior] = TRUE
|
||||
arguments.Cut(1, 2)
|
||||
if(length(arguments))
|
||||
behavior_args[behavior_type] = arguments
|
||||
else
|
||||
behavior_args -= behavior_type
|
||||
|
||||
if(!(behavior.behavior_flags & AI_BEHAVIOR_CAN_PLAN_DURING_EXECUTION)) // this one blocks planning!
|
||||
able_to_plan = FALSE
|
||||
|
||||
if(should_exit_unplanned)
|
||||
exit_unplanned_mode()
|
||||
|
||||
SEND_SIGNAL(src, AI_CONTROLLER_BEHAVIOR_QUEUED(behavior_type), arguments)
|
||||
|
||||
/datum/ai_controller/proc/check_able_to_plan()
|
||||
for(var/datum/ai_behavior/current_behavior as anything in current_behaviors)
|
||||
if(!(current_behavior.behavior_flags & AI_BEHAVIOR_CAN_PLAN_DURING_EXECUTION)) // We have a behavior that blocks planning
|
||||
return FALSE
|
||||
return TRUE
|
||||
|
||||
/datum/ai_controller/proc/dequeue_behavior(datum/ai_behavior/behavior)
|
||||
current_behaviors -= behavior
|
||||
able_to_plan = check_able_to_plan()
|
||||
if(!length(current_behaviors))
|
||||
enter_unplanned_mode()
|
||||
|
||||
/datum/ai_controller/proc/exit_unplanned_mode()
|
||||
remove_from_unplanned_controllers()
|
||||
start_ai_processing()
|
||||
|
||||
/datum/ai_controller/proc/enter_unplanned_mode()
|
||||
add_to_unplanned_controllers()
|
||||
stop_previous_processing()
|
||||
|
||||
/datum/ai_controller/proc/process_behavior(seconds_per_tick, datum/ai_behavior/behavior)
|
||||
var/list/arguments = list(seconds_per_tick, src)
|
||||
var/list/stored_arguments = behavior_args[behavior.type]
|
||||
@@ -460,7 +586,7 @@ RESTRICT_TYPE(/datum/ai_controller)
|
||||
behavior.finish_action(arglist(arguments))
|
||||
|
||||
/datum/ai_controller/proc/cancel_actions()
|
||||
if(!LAZYLEN(current_behaviors))
|
||||
if(!length(current_behaviors))
|
||||
return
|
||||
for(var/datum/ai_behavior/current_behavior as anything in current_behaviors)
|
||||
fail_behavior(current_behavior)
|
||||
|
||||
@@ -74,7 +74,7 @@
|
||||
//Update the ignore_if_not_on_turf
|
||||
AddComponent(/datum/component/connect_range, host, loc_connections, current_range, ignore_if_not_on_turf)
|
||||
|
||||
/datum/proximity_monitor/proc/on_uncrossed()
|
||||
/datum/proximity_monitor/proc/on_uncrossed(atom/source, atom/movable/exited)
|
||||
SIGNAL_HANDLER // COMSIG_ATOM_EXITED
|
||||
return //Used by the advanced subtype for effect fields.
|
||||
|
||||
|
||||
@@ -149,6 +149,9 @@
|
||||
|
||||
moveToNullspace()
|
||||
|
||||
if(spatial_grid_key)
|
||||
SSspatial_grid.force_remove_from_grid(src)
|
||||
|
||||
// This absolutely must be after moveToNullspace()
|
||||
// We rely on Entered and Exited to manage this list, and the copy of this list that is on any /atom/movable "Containers"
|
||||
// If we clear this before the nullspace move, a ref to this object will be hung in any of its movable containers
|
||||
|
||||
@@ -44,8 +44,8 @@
|
||||
ai_movement = /datum/ai_movement/jps // Not gonna get stuck on walls now, are ya?
|
||||
idle_behavior = /datum/idle_behavior/idle_random_walk
|
||||
planning_subtrees = list(
|
||||
/datum/ai_planning_subtree/simple_find_target,
|
||||
/datum/ai_planning_subtree/basic_melee_attack_subtree,
|
||||
/datum/ai_planning_subtree/simple_find_target,
|
||||
)
|
||||
|
||||
/// Same as hivelords, but go after corpses too
|
||||
@@ -118,17 +118,21 @@
|
||||
if(!target)
|
||||
// Only summon if we have a hostile target
|
||||
return
|
||||
controller.queue_behavior(/datum/ai_behavior/summon_brood)
|
||||
controller.queue_behavior(/datum/ai_behavior/summon_brood, target_key)
|
||||
|
||||
/datum/ai_behavior/summon_brood
|
||||
action_cooldown = 3 SECONDS
|
||||
behavior_flags = AI_BEHAVIOR_MOVE_AND_PERFORM
|
||||
required_distance = 0
|
||||
|
||||
/datum/ai_behavior/summon_brood/perform(seconds_per_tick, datum/ai_controller/controller)
|
||||
/datum/ai_behavior/summon_brood/perform(seconds_per_tick, datum/ai_controller/controller, target_key)
|
||||
. = ..()
|
||||
var/mob/living/basic/mining/hivelord/summoner = controller.pawn
|
||||
var/mob/living/basic/mining/hivelordbrood/A = new summoner.brood_type(summoner.loc)
|
||||
A.admin_spawned = summoner.admin_spawned
|
||||
A.faction = summoner.faction.Copy()
|
||||
var/mob/living/target = controller.blackboard[target_key]
|
||||
if(istype(target) && istype(A.ai_controller))
|
||||
A.ai_controller.set_ai_status(AI_STATUS_ON)
|
||||
A.ai_controller.set_blackboard_key(target_key, target)
|
||||
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_SUCCEEDED
|
||||
|
||||
@@ -1084,7 +1084,7 @@
|
||||
var/old_level_new_clients = (registered_z ? length(SSmobs.clients_by_zlevel[registered_z]) : null)
|
||||
// No one is left after we're gone, shut off inactive ones
|
||||
if(registered_z && old_level_new_clients == 0)
|
||||
for(var/datum/ai_controller/controller as anything in SSai_controllers.ai_controllers_by_zlevel[registered_z])
|
||||
for(var/datum/ai_controller/controller as anything in GLOB.ai_controllers_by_zlevel[registered_z])
|
||||
controller.set_ai_status(AI_STATUS_OFF)
|
||||
|
||||
|
||||
@@ -1097,7 +1097,7 @@
|
||||
|
||||
if(new_level_old_clients == 0) // No one was here before, wake up all the AIs.
|
||||
// Basic mob AI
|
||||
for(var/datum/ai_controller/controller as anything in SSai_controllers.ai_controllers_by_zlevel[new_z])
|
||||
for(var/datum/ai_controller/controller as anything in GLOB.ai_controllers_by_zlevel[new_z])
|
||||
// We don't set them directly on, for instances like AIs acting while dead and other cases that may exist in the future.
|
||||
// This isn't a problem for AIs with a client since the client will prevent this from being called anyway.
|
||||
controller.set_ai_status(controller.get_expected_ai_status())
|
||||
|
||||
@@ -49,11 +49,11 @@
|
||||
"uid" = "[movement_target.UID()]",
|
||||
"source" = "[controller.movement_target_source]",
|
||||
)
|
||||
if(LAZYLEN(controller.current_behaviors))
|
||||
if(length(controller.current_behaviors))
|
||||
for(var/datum/ai_behavior/behavior in controller.current_behaviors)
|
||||
data["controller"]["current_behaviors"] += "[behavior.type]"
|
||||
|
||||
if(LAZYLEN(controller.planned_behaviors))
|
||||
if(length(controller.planned_behaviors))
|
||||
for(var/datum/ai_behavior/behavior in controller.planned_behaviors)
|
||||
data["controller"]["planned_behaviors"] += "[behavior.type]"
|
||||
|
||||
|
||||
@@ -239,6 +239,7 @@
|
||||
#include "code\_globalvars\mapping_vars.dm"
|
||||
#include "code\_globalvars\misc_globals.dm"
|
||||
#include "code\_globalvars\traits.dm"
|
||||
#include "code\_globalvars\lists\basic_ai_lists.dm"
|
||||
#include "code\_globalvars\lists\dye_registry.dm"
|
||||
#include "code\_globalvars\lists\flavor_misc.dm"
|
||||
#include "code\_globalvars\lists\fortunes.dm"
|
||||
@@ -322,6 +323,7 @@
|
||||
#include "code\controllers\configuration\sections\tgui_configuration.dm"
|
||||
#include "code\controllers\configuration\sections\url_configuration.dm"
|
||||
#include "code\controllers\configuration\sections\vote_configuration.dm"
|
||||
#include "code\controllers\subsystem\ai_idle_controllers.dm"
|
||||
#include "code\controllers\subsystem\SSacid.dm"
|
||||
#include "code\controllers\subsystem\SSafk.dm"
|
||||
#include "code\controllers\subsystem\SSai_controllers.dm"
|
||||
@@ -369,6 +371,8 @@
|
||||
#include "code\controllers\subsystem\SSticker.dm"
|
||||
#include "code\controllers\subsystem\SStime_track.dm"
|
||||
#include "code\controllers\subsystem\SStimer.dm"
|
||||
#include "code\controllers\subsystem\SSunplanned_ai_idle_controllers.dm"
|
||||
#include "code\controllers\subsystem\SSunplanned_controllers.dm"
|
||||
#include "code\controllers\subsystem\SSverb_manager.dm"
|
||||
#include "code\controllers\subsystem\SSvis_overlays.dm"
|
||||
#include "code\controllers\subsystem\SSvote.dm"
|
||||
@@ -393,6 +397,7 @@
|
||||
#include "code\controllers\subsystem\non_firing\SSspatial_grid.dm"
|
||||
#include "code\controllers\subsystem\non_firing\SStitlescreen.dm"
|
||||
#include "code\controllers\subsystem\processing\SSai_behaviors.dm"
|
||||
#include "code\controllers\subsystem\processing\SSai_idle_behaviors.dm"
|
||||
#include "code\controllers\subsystem\processing\SSdcs.dm"
|
||||
#include "code\controllers\subsystem\processing\SSfastprocess.dm"
|
||||
#include "code\controllers\subsystem\processing\SSfields.dm"
|
||||
|
||||
Reference in New Issue
Block a user