diff --git a/code/__DEFINES/vv.dm b/code/__DEFINES/vv.dm
index 13d91141c5d..adb1ce88fe7 100644
--- a/code/__DEFINES/vv.dm
+++ b/code/__DEFINES/vv.dm
@@ -45,6 +45,7 @@
#define VV_HK_EDITREAGENTS "editreagents"
#define VV_HK_EXPLODE "explode"
#define VV_HK_EMP "emp"
+#define VV_HK_DEBUG_AI_CONTROLLER "debug_ai_controller"
// /obj
#define VV_HK_DELALL "delall"
diff --git a/code/game/atom_vv.dm b/code/game/atom_vv.dm
index 4a0f7068a49..394f7998996 100644
--- a/code/game/atom_vv.dm
+++ b/code/game/atom_vv.dm
@@ -17,6 +17,8 @@
VV_DROPDOWN_OPTION(VV_HK_EDITREAGENTS, "Edit reagents")
VV_DROPDOWN_OPTION(VV_HK_EXPLODE, "Trigger explosion")
VV_DROPDOWN_OPTION(VV_HK_EMP, "Trigger EM pulse")
+ if(istype(ai_controller))
+ VV_DROPDOWN_OPTION(VV_HK_DEBUG_AI_CONTROLLER, "Debug AI Controller")
/atom/proc/vv_modify_name_link()
return "byond://?_src_=vars;datumedit=[UID()];varnameedit=name"
@@ -100,3 +102,14 @@
message_admins("[key_name_admin(usr)] is manipulating the colour matrix for [src]")
var/datum/ui_module/colour_matrix_tester/CMT = new(target=src)
CMT.ui_interact(usr)
+
+ if(href_list[VV_HK_DEBUG_AI_CONTROLLER])
+ if(!check_rights(R_DEBUG|R_DEV_TEAM))
+ return
+
+ if(!istype(ai_controller))
+ to_chat(usr, "Could not find atom [href_list[VV_HK_DEBUG_AI_CONTROLLER]] with AI controller")
+ return
+
+ var/datum/ui_module/ai_controller_debugger/debugger = new(ai_controller)
+ debugger.ui_interact(usr)
diff --git a/code/modules/admin/admin_verbs.dm b/code/modules/admin/admin_verbs.dm
index 928045eba68..492f806cc19 100644
--- a/code/modules/admin/admin_verbs.dm
+++ b/code/modules/admin/admin_verbs.dm
@@ -1305,3 +1305,23 @@ GLOBAL_LIST_INIT(view_logs_verbs, list(
B.to_backup_disk(get_turf(usr))
+/proc/ghost_follow_uid(mob/user, uid)
+ var/client/client = user.client
+ if(!isobserver(user))
+ if(!check_rights(R_ADMIN|R_MOD)) // Need to be mod or admin to aghost
+ return
+ user.client.admin_ghost()
+ var/datum/target = locateUID(uid)
+ if(QDELETED(target))
+ to_chat(user, "This datum has been deleted!")
+ return
+
+ if(istype(target, /datum/mind))
+ var/datum/mind/mind = target
+ if(!ismob(mind.current))
+ to_chat(user, "This can only be used on instances of type /mob")
+ return
+ target = mind.current
+
+ var/mob/dead/observer/A = client.mob
+ A.ManualFollow(target)
diff --git a/code/modules/admin/menus/antagonist_menu.dm b/code/modules/admin/menus/antagonist_menu.dm
index 2a5c7021f17..6d62a4c6d16 100644
--- a/code/modules/admin/menus/antagonist_menu.dm
+++ b/code/modules/admin/menus/antagonist_menu.dm
@@ -149,25 +149,7 @@ RESTRICT_TYPE(/datum/ui_module/admin/antagonist_menu)
if("pm")
ui.user.client.cmd_admin_pm(params["ckey"], null)
if("follow")
- var/client/C = ui.user.client
- if(!isobserver(ui.user))
- if(!check_rights(R_ADMIN|R_MOD)) // Need to be mod or admin to aghost
- return
- C.admin_ghost()
- var/datum/target = locateUID(params["datum_uid"])
- if(QDELETED(target))
- to_chat(ui.user, "This datum has been deleted!")
- return
-
- if(istype(target, /datum/mind))
- var/datum/mind/mind = target
- if(!ismob(mind.current))
- to_chat(ui.user, "This can only be used on instances of type /mob")
- return
- target = mind.current
-
- var/mob/dead/observer/A = C.mob
- A.ManualFollow(target)
+ ghost_follow_uid(ui.user, params["datum_uid"])
if("obs")
var/client/C = ui.user.client
var/datum/mind/mind = locateUID(params["mind_uid"])
diff --git a/code/modules/tgui/modules/ai_controller_debugger.dm b/code/modules/tgui/modules/ai_controller_debugger.dm
new file mode 100644
index 00000000000..b1df46140cd
--- /dev/null
+++ b/code/modules/tgui/modules/ai_controller_debugger.dm
@@ -0,0 +1,88 @@
+/datum/ui_module/ai_controller_debugger
+ name = "AI Controller Debugger"
+ var/datum/ai_controller/controller
+ var/paused = FALSE
+ var/list/data = list()
+
+/datum/ui_module/ai_controller_debugger/New(datum/ai_controller/controller_)
+ ..()
+ if(!istype(controller_))
+ stack_trace("Attempted to create an AI controller debugger on an invalid target!")
+ qdel(src)
+ return
+
+ controller = controller_
+
+/datum/ui_module/ai_controller_debugger/ui_state(mob/user)
+ return GLOB.admin_state
+
+/datum/ui_module/ai_controller_debugger/ui_interact(mob/user, datum/tgui/ui)
+ ui = SStgui.try_update_ui(user, src, ui)
+ if(!ui)
+ ui = new(user, src, "AIControllerDebugger", name)
+ ui_data(user)
+ ui.open()
+
+/datum/ui_module/ai_controller_debugger/ui_data(mob/user)
+ data["paused"] = paused
+ if(paused)
+ return data
+
+ data["controller"] = list(
+ "type" = "[controller.type]",
+ "current_behaviors" = list(),
+ "planned_behaviors" = list(),
+ "blackboard" = list(),
+ )
+ if(controller.pawn)
+ data["controller"]["pawn"] = list(
+ "name" = "[controller.pawn]",
+ "uid" = controller.pawn.UID(),
+ )
+ data["controller"]["type"] = "[controller.type]"
+ data["controller"]["idle_behavior"] = "[controller.idle_behavior]"
+ data["controller"]["movement"] = "[controller.ai_movement]"
+ var/datum/movement_target = controller.current_movement_target
+ if(istype(movement_target))
+ data["controller"]["movement_target"] = list(
+ "name" = "[movement_target]",
+ "uid" = "[movement_target.UID()]",
+ "source" = "[controller.movement_target_source]",
+ )
+ if(LAZYLEN(controller.current_behaviors))
+ for(var/datum/ai_behavior/behavior in controller.current_behaviors)
+ data["controller"]["current_behaviors"] += "[behavior.type]"
+
+ if(LAZYLEN(controller.planned_behaviors))
+ for(var/datum/ai_behavior/behavior in controller.planned_behaviors)
+ data["controller"]["planned_behaviors"] += "[behavior.type]"
+
+ for(var/name in controller.blackboard)
+ var/value = controller.blackboard[name]
+
+ var/list/item = list(
+ "name" = name,
+ "value" = "[value]",
+ )
+ var/datum/valid_uid = locateUID(value)
+ if(istype(valid_uid))
+ item["uid"] = value
+ if(isdatum(value))
+ var/datum/D = value
+ item["uid"] = D.UID()
+ if(islist(value))
+ item["value"] = json_encode(value)
+ data["controller"]["blackboard"] += list(item)
+ return data
+
+/datum/ui_module/ai_controller_debugger/ui_act(action, list/params, datum/tgui/ui, datum/ui_state/state)
+ if(..())
+ return
+
+ switch(action)
+ if("vv")
+ ui.user.client.debug_variables(locateUID(params["uid"]))
+ if("flw")
+ ghost_follow_uid(ui.user, params["uid"])
+
+ return TRUE
diff --git a/paradise.dme b/paradise.dme
index f9b96e7602c..d00486faa93 100644
--- a/paradise.dme
+++ b/paradise.dme
@@ -3180,6 +3180,7 @@
#include "code\modules\tgui\states.dm"
#include "code\modules\tgui\tgui_datum.dm"
#include "code\modules\tgui\tgui_window.dm"
+#include "code\modules\tgui\modules\ai_controller_debugger.dm"
#include "code\modules\tgui\modules\appearance_changer.dm"
#include "code\modules\tgui\modules\atmos_control.dm"
#include "code\modules\tgui\modules\colour_matrix_tester.dm"
diff --git a/tgui/packages/tgui/interfaces/AIControllerDebugger.tsx b/tgui/packages/tgui/interfaces/AIControllerDebugger.tsx
new file mode 100644
index 00000000000..ce20322ed90
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/AIControllerDebugger.tsx
@@ -0,0 +1,166 @@
+import { ReactNode } from 'react';
+import { Box, Button, Flex, LabeledList, NoticeBox, Section, Stack, Table, Tabs, Tooltip } from 'tgui-core/components';
+import { BooleanLike } from 'tgui-core/react';
+
+import { useBackend } from '../backend';
+import { Window } from '../layouts';
+import { truncate } from 'lodash';
+
+interface AIControllerDebuggerData {
+ controller: AIController;
+}
+
+interface ObjRef {
+ name: string;
+ uid: string;
+}
+
+interface AIController {
+ type: string;
+ pawn?: ObjRef;
+ idle_behavior: string;
+ movement: string;
+ movement_target?: MovementTarget;
+ current_behaviors: string[];
+ planned_behaviors: string[];
+ blackboard: BlackboardItem[];
+}
+
+interface MovementTarget extends ObjRef {
+ source: string;
+}
+
+interface BlackboardItem {
+ name: string;
+ value: string;
+ uid?: string;
+}
+
+export const CopyableValue = ({ text }) => {
+ return (
+ <>
+