refactors admin narrate (#7319)

This commit is contained in:
silicons
2025-10-08 14:51:38 -07:00
committed by GitHub
parent 8fedefc9d3
commit f76aeb8f33
35 changed files with 1207 additions and 224 deletions

View File

@@ -29,5 +29,6 @@
},
"[javascript][typescript][typescriptreact][javascriptreact]": {
"editor.defaultFormatter": "vscode.typescript-language-features"
}
},
"editor.formatOnSave": true
}

View File

@@ -34,7 +34,6 @@
#include "code\__DEFINES\ability.dm"
#include "code\__DEFINES\access.dm"
#include "code\__DEFINES\actionspeed_modification.dm"
#include "code\__DEFINES\admin_verb.dm"
#include "code\__DEFINES\appearance.dm"
#include "code\__DEFINES\assert.dm"
#include "code\__DEFINES\automata.dm"
@@ -126,6 +125,7 @@
#include "code\__DEFINES\admin\admin.dm"
#include "code\__DEFINES\admin\bans.dm"
#include "code\__DEFINES\admin\keybindings.dm"
#include "code\__DEFINES\admin\verbs.dm"
#include "code\__DEFINES\ai\ai_holder.dm"
#include "code\__DEFINES\ai\debug.dm"
#include "code\__DEFINES\assets\medical.dm"
@@ -480,9 +480,9 @@
#include "code\__HELPERS\matrices\color_matrix.dm"
#include "code\__HELPERS\matrices\transform_matrix.dm"
#include "code\__HELPERS\misc\sonar.dm"
#include "code\__HELPERS\pathfinding\astar.dm"
#include "code\__HELPERS\pathfinding\common.dm"
#include "code\__HELPERS\pathfinding\jps.dm"
#include "code\__HELPERS\pathfinding\pathfinding.dm"
#include "code\__HELPERS\pathfinding\algorithms\astar.dm"
#include "code\__HELPERS\pathfinding\algorithms\jps.dm"
#include "code\__HELPERS\rendering\positioning.dm"
#include "code\__HELPERS\rendering\screen_loc.dm"
#include "code\__HELPERS\rendering\view.dm"
@@ -1126,6 +1126,7 @@
#include "code\game\atoms\defense_old.dm"
#include "code\game\atoms\movement.dm"
#include "code\game\atoms\say.dm"
#include "code\game\atoms\movable\movable-admin.dm"
#include "code\game\atoms\movable\movable-logging.dm"
#include "code\game\atoms\movable\movable-movement.dm"
#include "code\game\atoms\movable\movable-proto_kinetic.dm"
@@ -2090,6 +2091,7 @@
#include "code\game\objects\structures\window_spawner.dm"
#include "code\game\objects\structures\crates_lockers\__closet.dm"
#include "code\game\objects\structures\crates_lockers\_closet_appearance_definitions.dm"
#include "code\game\objects\structures\crates_lockers\closet-admin.dm"
#include "code\game\objects\structures\crates_lockers\crates.dm"
#include "code\game\objects\structures\crates_lockers\largecrate.dm"
#include "code\game\objects\structures\crates_lockers\vehiclecage.dm"
@@ -2296,10 +2298,13 @@
#include "code\modules\actionspeed\modifiers\base.dm"
#include "code\modules\admin\admin.dm"
#include "code\modules\admin\admin_attack_log.dm"
#include "code\modules\admin\admin_holder-legacy.dm"
#include "code\modules\admin\admin_holder.dm"
#include "code\modules\admin\admin_memo.dm"
#include "code\modules\admin\admin_ranks.dm"
#include "code\modules\admin\admin_secrets.dm"
#include "code\modules\admin\admin_tools.dm"
#include "code\modules\admin\admin_verb_descriptor.dm"
#include "code\modules\admin\admin_verbs.dm"
#include "code\modules\admin\banjob.dm"
#include "code\modules\admin\chat_commands.dm"
@@ -2307,7 +2312,6 @@
#include "code\modules\admin\create_mob.dm"
#include "code\modules\admin\create_object.dm"
#include "code\modules\admin\create_turf.dm"
#include "code\modules\admin\holder2.dm"
#include "code\modules\admin\investigate.dm"
#include "code\modules\admin\IsBanned.dm"
#include "code\modules\admin\map_capture.dm"
@@ -2317,6 +2321,8 @@
#include "code\modules\admin\player_panel.dm"
#include "code\modules\admin\topic.dm"
#include "code\modules\admin\ToRban.dm"
#include "code\modules\admin\admin_modal\admin_modal.dm"
#include "code\modules\admin\admin_modal\modals\admin_narrate.dm"
#include "code\modules\admin\ban\ban_system.dm"
#include "code\modules\admin\ban\role_ban.dm"
#include "code\modules\admin\ban\server_ban.dm"
@@ -2392,11 +2398,14 @@
#include "code\modules\admin\verbs\striketeam.dm"
#include "code\modules\admin\verbs\ticklag.dm"
#include "code\modules\admin\verbs\tripAI.dm"
#include "code\modules\admin\verbs\admin\delete.dm"
#include "code\modules\admin\verbs\debug\fucky_wucky.dm"
#include "code\modules\admin\verbs\debug\profiling.dm"
#include "code\modules\admin\verbs\debug\reestablish_db_connection.dm"
#include "code\modules\admin\verbs\debug\reload_configuration.dm"
#include "code\modules\admin\verbs\debug\spawn.dm"
#include "code\modules\admin\verbs\game\narrate.dm"
#include "code\modules\admin\verbs\game\narrate_quick.dm"
#include "code\modules\admin\verbs\SDQL2\SDQL_2.dm"
#include "code\modules\admin\verbs\SDQL2\SDQL_2_parser.dm"
#include "code\modules\admin\verbs\SDQL2\SDQL_2_wrappers.dm"
@@ -3766,6 +3775,7 @@
#include "code\modules\metrics\api\spatial_series.dm"
#include "code\modules\metrics\api\string_set.dm"
#include "code\modules\metrics\api\time_series.dm"
#include "code\modules\metrics\metrics\admin.dm"
#include "code\modules\metrics\metrics\controllers.dm"
#include "code\modules\mining\mine_turfs-defense.dm"
#include "code\modules\mining\mine_turfs.dm"
@@ -3807,6 +3817,7 @@
#include "code\modules\mob\holder.dm"
#include "code\modules\mob\inventory_legacy.dm"
#include "code\modules\mob\life.dm"
#include "code\modules\mob\mob-admin.dm"
#include "code\modules\mob\mob-client.dm"
#include "code\modules\mob\mob-damage.dm"
#include "code\modules\mob\mob-defense.dm"
@@ -5433,6 +5444,7 @@
#include "code\modules\tgui\modules\specific\lathe_control.dm"
#include "code\modules\tgui\modules\specific\prosfab_control.dm"
#include "code\modules\tgui\states\admin.dm"
#include "code\modules\tgui\states\admin_modal_state.dm"
#include "code\modules\tgui\states\always.dm"
#include "code\modules\tgui\states\conscious.dm"
#include "code\modules\tgui\states\contained.dm"
@@ -5475,6 +5487,7 @@
#include "code\modules\vehicles\actions.dm"
#include "code\modules\vehicles\ridden.dm"
#include "code\modules\vehicles\sealed.dm"
#include "code\modules\vehicles\vehicle-admin.dm"
#include "code\modules\vehicles\vehicle-input.dm"
#include "code\modules\vehicles\vehicle-physics.dm"
#include "code\modules\vehicles\vehicle.dm"

View File

@@ -0,0 +1,59 @@
//* This file is explicitly licensed under the MIT license. *//
//* Copyright (c) 2024 Citadel Station Developers *//
/**
* Declares an admin verb.
*
* * Verbs declared in this way will have the caller as `client/caller`. Do not define your
* own client / usr calls.
* * You may safely assume that the verb is only accessible by them if they have the right permissions.
* * Set `CATEGORY` to null to not have it show up in verb panel.
*/
#define ADMIN_VERB_DEF(PATH_SUFFIX, REQUIRED_RIGHTS, NAME, DESC, CATEGORY, HEADER...) \
ADMIN_VERB_DEF_INTERNAL(PATH_SUFFIX, REQUIRED_RIGHTS, NAME, DESC, CATEGORY, TRUE, HEADER)
/**
* Declares an admin verb that does not show up in the popup menu.
*
* * Verbs declared in this way will have the caller as `client/caller`. Do not define your
* own client / usr calls.
* * You may safely assume that the verb is only accessible by them if they have the right permissions.
* * Set `CATEGORY` to null to not have it show up in verb panel.
*/
#define ADMIN_VERB_DEF_PANEL_ONLY(PATH_SUFFIX, REQUIRED_RIGHTS, NAME, DESC, CATEGORY, HEADER...) \
ADMIN_VERB_DEF_INTERNAL(PATH_SUFFIX, REQUIRED_RIGHTS, NAME, DESC, CATEGORY, FALSE, HEADER)
/**
* Do not use.
*/
#define ADMIN_VERB_DEF_INTERNAL(PATH_SUFFIX, REQUIRED_RIGHTS, NAME, DESC, CATEGORY, POPUP_MENU, HEADER...) \
/datum/admin_verb_descriptor/##PATH_SUFFIX { \
id = #PATH_SUFFIX; \
required_rights = ##REQUIRED_RIGHTS; \
verb_path = /datum/admin_verb_abstraction/proc/verb__##PATH_SUFFIX; \
reflection_path = /datum/admin_verb_abstraction/proc/verb__invoke_##PATH_SUFFIX; \
}; \
/datum/admin_verb_abstraction/proc/verb__##PATH_SUFFIX(##HEADER) { \
set name = NAME; \
set desc = DESC; \
set category = CATEGORY; \
set hidden = FALSE; \
set popup_menu = POPUP_MENU; \
if(!((usr?.client?.holder?.rights & ##REQUIRED_RIGHTS) == ##REQUIRED_RIGHTS)) {\
CRASH("attempted invocation with insufficient rights."); \
}; \
do { \
metric_increment_nested_numerical(/datum/metric/nested_numerical/admin_verb_invocation, #PATH_SUFFIX, 1); \
}; \
while(FALSE); \
call(usr.client, /datum/admin_verb_abstraction::verb__invoke_##PATH_SUFFIX())(arglist(list(usr.client) + args)); \
}; \
/datum/admin_verb_abstraction/proc/verb__invoke_##PATH_SUFFIX(client/caller, ##HEADER)
/**
* Abstract datum with no variables. Used to hold the proc definitions for admin verbs.
*
* * The way this works is black magic, so, uh, no touchy I guess.
*/
/datum/admin_verb_abstraction
abstract_type = /datum/admin_verb_abstraction

View File

@@ -1,24 +0,0 @@
// This port is currently half-assed, we do not include the entire avd stuff
/// Use this to mark your verb as not having a description. Should ONLY be used if you are also hiding the verb!
#define ADMIN_VERB_NO_DESCRIPTION ""
/// Used to verbs you do not want to show up in the master verb panel.
#define ADMIN_CATEGORY_HIDDEN null
// Admin verb categories
#define ADMIN_CATEGORY_MAIN "Admin"
#define ADMIN_CATEGORY_EVENTS "Admin.Events"
#define ADMIN_CATEGORY_FUN "Admin.Fun"
#define ADMIN_CATEGORY_GAME "Admin.Game"
#define ADMIN_CATEGORY_SHUTTLE "Admin.Shuttle"
// Special categories that are separated
#define ADMIN_CATEGORY_DEBUG "Debug"
#define ADMIN_CATEGORY_SERVER "Server"
#define ADMIN_CATEGORY_OBJECT "Object"
#define ADMIN_CATEGORY_MAPPING "Mapping"
#define ADMIN_CATEGORY_PROFILE "Profile"
#define ADMIN_CATEGORY_IPINTEL "Admin.IPIntel"
// Visibility flags
#define ADMIN_VERB_VISIBLITY_FLAG_MAPPING_DEBUG "Map-Debug"

View File

@@ -7,3 +7,8 @@
#define VERB_CATEGORY_SYSTEM "System"
// todo: admin verb categories too
#define VERB_CATEGORY_ADMIN "Admin"
#define VERB_CATEGORY_DEBUG "Debug"
#define VERB_CATEGORY_GAME "Game"
#define VERB_CATEGORY_SERVER "Server"

View File

@@ -3,4 +3,5 @@
//* Metric Categories *//
#define METRIC_CATEGORY_ADMIN "admin"
#define METRIC_CATEGORY_SERVER "server"

View File

@@ -1,7 +1,6 @@
// todo: DO NOT FUCKING USE THIS
// it is *EXTREMELY* inefficient, and scales up quadratically in time complexity
// DO NOT USE THIS UNTIL IT IS REWRITTEN
// notably that "bad node trimming" is actually horrifying.
// TODO: optimize this for use on actual graphs; make it efficient and generic.
// while we do not currently have a use for generic graph-search, we sure as hell
// will eventually.
/**
* A Star pathfinding algorithm

View File

@@ -0,0 +1,8 @@
//* This file is explicitly licensed under the MIT license. *//
//* Copyright (c) 2025 Citadel Station Developers *//
/**
* @return null if not supported, or list of mobs to target
*/
/atom/movable/proc/admin_resolve_narrate() as /list
return null

View File

@@ -836,19 +836,6 @@ modules/mob/living/carbon/human/life.dm if you die, you will be zoomed out.
if(isturf(loc) && I.obj_storage?.allow_mass_gather && I.obj_storage.allow_mass_gather_via_click)
I.obj_storage.auto_handle_interacted_mass_pickup(new /datum/event_args/actor(user), src)
return CLICKCHAIN_DO_NOT_PROPAGATE | CLICKCHAIN_DID_SOMETHING
if(istype(I, /obj/item/cell) && !isnull(obj_cell_slot) && isnull(obj_cell_slot.cell) && obj_cell_slot.interaction_active(user))
if(!user.transfer_item_to_loc(I, src))
user.action_feedback(SPAN_WARNING("[I] is stuck to your hand!"), src)
return CLICKCHAIN_DO_NOT_PROPAGATE
user.visible_action_feedback(
target = src,
hard_range = obj_cell_slot.remove_is_discrete? 0 : MESSAGE_RANGE_CONSTRUCTION,
visible_hard = SPAN_NOTICE("[user] inserts [I] into [src]."),
audible_hard = SPAN_NOTICE("You hear something being slotted in."),
visible_self = SPAN_NOTICE("You insert [I] into [src]."),
)
obj_cell_slot.insert_cell(I)
return CLICKCHAIN_DO_NOT_PROPAGATE | CLICKCHAIN_DID_SOMETHING
return ..()
/**

View File

@@ -404,6 +404,12 @@
if(!user.transfer_item_to_loc(I, src))
user.action_feedback(SPAN_WARNING("[I] is stuck to your hand!"), src)
return CLICKCHAIN_DO_NOT_PROPAGATE
if(!obj_cell_slot.accepts_cell(I))
user.action_feedback(
SPAN_WARNING("[src] do,es not accept [I]."),
target = src,
)
return CLICKCHAIN_DO_NOT_PROPAGATE
user.visible_action_feedback(
target = src,
hard_range = obj_cell_slot.remove_is_discrete? 0 : MESSAGE_RANGE_CONSTRUCTION,

View File

@@ -0,0 +1,7 @@
//* This file is explicitly licensed under the MIT license. *//
//* Copyright (c) 2025 Citadel Station Developers *//
/obj/structure/closet/admin_resolve_narrate()
. = list()
for(var/mob/target in .)
. += target

View File

@@ -48,6 +48,9 @@
/datum/object_system/cell_slot/proc/accepts_cell(obj/item/cell/cell)
return legacy_use_device_cells? istype(cell, /obj/item/cell/device) : TRUE
// TODO: user_remove_cell && user_insert_cell
// TODO: play sound please & visible message
/**
* removes cell from the system and drops it at new_loc
*/

View File

@@ -3,6 +3,7 @@ var/list/admin_datums = list()
GLOBAL_VAR_INIT(href_token, GenerateToken())
GLOBAL_PROTECT(href_token)
// todo: /datum/admin_holder
/datum/admins
var/rank = "Temporary Admin"
var/client/owner = null
@@ -36,27 +37,97 @@ GLOBAL_PROTECT(href_token)
spawn(-1)
UNTIL(SSmapping.loaded_station)
admincaster_signature = "[(LEGACY_MAP_DATUM).company_name] Officer #[rand(0,9)][rand(0,9)][rand(0,9)]"
..()
// todo: assertions on this are too weak
/datum/admins/proc/associate(client/C)
if(istype(C))
owner = C
owner.holder = src
owner.add_admin_verbs() //TODO
GLOB.admins |= C
if(owner == C)
return
if(owner)
disassociate()
if(!istype(C))
return
owner = C
owner.holder = src
GLOB.admins |= C
// add verbs
add_admin_verbs()
// todo: assertions on this are too weak
/datum/admins/proc/disassociate()
if(owner)
GLOB.admins -= owner
owner.remove_admin_verbs()
owner.deadmin_holder = owner.holder
owner.holder = null
if(!owner)
return
// for now, destroy all modals
QDEL_LIST(admin_modals)
// obliterate verbs
remove_admin_verbs()
GLOB.admins -= owner
owner.deadmin_holder = owner.holder
owner.holder = null
owner = null
/datum/admins/proc/reassociate()
if(owner)
GLOB.admins |= owner
owner.holder = src
owner.deadmin_holder = null
owner.add_admin_verbs()
/datum/admins/add_admin_verbs()
..()
if(!owner)
return
var/list/verbs_to_add = list()
verbs_to_add += admin_verbs_default
if(rights & R_BUILDMODE)
verbs_to_add += /client/proc/togglebuildmodeself
if(rights & R_ADMIN)
verbs_to_add += admin_verbs_admin
if(rights & R_BAN)
verbs_to_add += admin_verbs_ban
if(rights & R_FUN)
verbs_to_add += admin_verbs_fun
if(rights & R_SERVER)
verbs_to_add += admin_verbs_server
if(rights & R_DEBUG)
verbs_to_add += admin_verbs_debug
if(rights & R_POSSESS)
verbs_to_add += admin_verbs_possess
if(rights & R_PERMISSIONS)
verbs_to_add += admin_verbs_permissions
if(rights & R_STEALTH)
verbs_to_add += /client/proc/stealth
if(rights & R_REJUVINATE)
verbs_to_add += admin_verbs_rejuv
if(rights & R_SOUNDS)
verbs_to_add += admin_verbs_sounds
if(rights & R_SPAWN)
verbs_to_add += admin_verbs_spawn
if(rights & R_MOD)
verbs_to_add += admin_verbs_mod
if(rights & R_EVENT)
verbs_to_add += admin_verbs_event_manager
add_verb(
owner,
verbs_to_add,
)
/datum/admins/remove_admin_verbs()
..()
if(!owner)
return
remove_verb(
owner,
list(
admin_verbs_default,
/client/proc/togglebuildmodeself,
admin_verbs_admin,
admin_verbs_ban,
admin_verbs_fun,
admin_verbs_server,
admin_verbs_debug,
admin_verbs_possess,
admin_verbs_permissions,
/client/proc/stealth,
admin_verbs_rejuv,
admin_verbs_sounds,
admin_verbs_spawn,
debug_verbs,
),
)
/*
checks if usr is an admin with at least ONE of the flags in rights_required. (Note, they don't need all the flags)
@@ -119,7 +190,6 @@ NOTE: It checks usr by default. Supply the "user" argument if you wish to check
/client/proc/deadmin()
if(holder)
holder.disassociate()
//qdel(holder)
return 1
/proc/GenerateToken()
@@ -159,12 +229,3 @@ NOTE: It checks usr by default. Supply the "user" argument if you wish to check
return TRUE
log_admin_private("[key_name(usr)] clicked an href with [msg] authorization key! [href]")
*/
/datum/admins/vv_edit_var(var_name, var_value)
#ifdef TESTING
return ..()
#else
if(var_name == NAMEOF(src, rank) || var_name == NAMEOF(src, rights))
return FALSE
return ..()
#endif

View File

@@ -0,0 +1,76 @@
//* This file is explicitly licensed under the MIT license. *//
//* Copyright (c) 2025 Citadel Station Developers *//
/datum/admins
/// lazy list of active admin modals
///
/// todo: re-open these on reconnect.
var/list/datum/admin_modal/admin_modals
/// owning ckey
var/ckey
/datum/admins/Destroy()
QDEL_LIST(admin_modals)
return ..()
/datum/admins/New(initial_rank, initial_rights, ckey)
src.ckey = ckey
..()
/datum/admins/proc/add_admin_verbs()
if(!owner)
return
var/list/verbs_to_add = list()
for(var/datum/admin_verb_descriptor/descriptor in global.admin_verb_descriptors)
if((rights & descriptor.required_rights) != descriptor.required_rights)
continue
verbs_to_add += descriptor.verb_path
add_verb(
owner,
verbs_to_add,
)
world.SetConfig("APP/admin", ckey, "role=admin")
/datum/admins/proc/remove_admin_verbs()
var/list/verbs_to_remove = list()
for(var/datum/admin_verb_descriptor/descriptor in global.admin_verb_descriptors)
verbs_to_remove += descriptor.verb_path
remove_verb(
owner,
verbs_to_remove,
)
world.SetConfig("APP/admin", ckey, "null")
//* Admin Modals *//
/datum/admins/proc/open_admin_modal(path, ...)
ASSERT(ispath(path, /datum/admin_modal))
var/datum/admin_modal/modal = new path(src)
if(!modal.Initialize(arglist(args.Copy(2))))
qdel(modal)
message_admins("Failed to initialize an admin modal. Check runtimes for more details.")
stack_trace("failed to initialize an admin modal; this means someone passed in bad args.")
return null
modal.open()
return modal
//* -- SECURITY -- *//
//* Do not touch this section unless you know what you are doing. *//
/datum/admins/vv_edit_var(var_name, var_value)
#ifdef TESTING
return ..()
#else
if(var_name == NAMEOF(src, rank) || var_name == NAMEOF(src, rights))
return FALSE
return ..()
#endif
/datum/admins/CanProcCall(procname)
switch(procname)
if(NAMEOF_PROC(src, open_admin_modal))
return FALSE
if(findtext(procname, "verb__") == 1)
return FALSE
return ..()

View File

@@ -0,0 +1,70 @@
//* This file is explicitly licensed under the MIT license. *//
//* Copyright (c) 2024 Citadel Station Developers *//
//* Modal *//
/**
* Base type of admin modals, which tend to be standalone panels.
*/
VV_PROTECT_READONLY(/datum/admin_modal)
/datum/admin_modal
/// Our name, for UI / output purposes
var/name = "Unknown Modal"
/// The admin datum that opened us
var/datum/admins/owner
/// TGUI ID; this will always be loaded from `tgui/interfaces/admin_modal` if possible.
var/tgui_interface
/// Do we autoupdate?
var/tgui_update = TRUE
/// are we initialized?
var/initialized = FALSE
/datum/admin_modal/New(datum/admins/for_owner)
owner = for_owner
LAZYADD(owner.admin_modals, src)
/datum/admin_modal/Destroy()
LAZYREMOVE(owner.admin_modals, src)
return ..()
/datum/admin_modal/ui_interact(mob/user, datum/tgui/ui, datum/tgui/parent_ui)
ui = SStgui.try_update_ui(user, src, ui)
if(!ui)
ui = new(user, src, "admin_modal/[tgui_interface]")
ui.set_autoupdate(tgui_update)
ui.open()
/datum/admin_modal/ui_state()
return GLOB.ui_admin_modal_state
// TODO: don't destroy if it's part of a disconnect, restore after?
/datum/admin_modal/on_ui_close(mob/user, datum/tgui/ui, embedded)
..()
qdel(src)
/**
* Just a wrapper for opening our UI. Ensures everything is initialized.
*/
/datum/admin_modal/proc/open()
if(!initialized)
CRASH("attempted to open an uninitialized admin modal")
if(!owner?.owner?.mob)
return
ui_interact(owner.owner.mob)
/**
* Called with args to open_admin_modal().
*/
/datum/admin_modal/proc/Initialize(...)
initialized = TRUE
return TRUE
/**
* Call to complain about a failure in doing something.
*
* * Failures should generally be done UI-side, this is for when something is seriously wrong.
*/
/datum/admin_modal/proc/loud_rejection(message)
// todo: make this fancier
to_chat(owner.owner, "<font color='red'><b>[name]</b>: [message]</font>")

View File

@@ -0,0 +1,361 @@
//* This file is explicitly licensed under the MIT license. *//
//* Copyright (c) 2025 Citadel Station Developers *//
/datum/admin_modal/admin_narrate
name = "Admin Narrate"
/**
* Target. Also determines what modes we can change to.
* ### in global, lobby mode
* Ignored.
* ### in overmap mode
* Can be atom, map_level, map, or overmap entity
* ### in sector mode
* Can be atom, map_level, map
* ### in level mode
* Can be atom, map_level
* ### in range mode
* Can be atom
* ### in direct mode
* Can be movable that is a or can hold mob(s), or client
*/
var/atom/target
//* i'm so sorry lohikar but i don't believe in define spam for this *//
var/const/M_GLOBAL = "global"
var/const/M_SECTOR = "sector"
var/const/M_OVERMAP = "overmap"
var/const/M_LEVEL = "level"
var/const/M_RANGE = "range"
var/const/M_DIRECT = "direct"
var/const/M_LOBBY = "lobby"
/// text to send (raw html)
var/unsafe_raw_html_to_send
/// narrate mode
var/mode
/// use line of sight when doing in range?
var/use_los = FALSE
/// use range in tiles; in overmaps, this is multiples of WORLD_ICON_SIZE
var/use_range = 14
var/const/MAX_LOS_RANGE = 35
var/const/MAX_ANY_RANGE = 1024
/// our identifying color
var/narrate_visual_color
/// our target image
// TODO: visualize it
// var/image/narrate_target_image
/datum/admin_modal/admin_narrate/Initialize(atom/target)
. = ..()
if(!.)
return
set_target(target)
src.narrate_visual_color = rgb(arglist(hsl2rgb(rand(0, 360), rand(0, 360), rand(125, 360))))
var/list/possible_modes = resolve_modes()
if(length(possible_modes))
src.mode = possible_modes[1]
else
return FALSE
/datum/admin_modal/admin_narrate/Destroy()
set_target(null)
return ..()
/datum/admin_modal/admin_narrate/proc/set_target(atom/target)
if(src.target)
on_unset_target(src.target)
src.target = target
if(src.target)
on_set_target(src.target)
/datum/admin_modal/admin_narrate/proc/on_unset_target(atom/new_target)
// TODO: visualize it
UnregisterSignal(target, COMSIG_PARENT_QDELETING)
/datum/admin_modal/admin_narrate/proc/on_set_target(atom/new_target)
// TODO: visualize it
RegisterSignal(target, COMSIG_PARENT_QDELETING, PROC_REF(on_target_del))
/datum/admin_modal/admin_narrate/proc/on_target_del(datum/source)
if(source != target)
CRASH("how was target del signal invoked by something that is not our target?")
set_target(null)
/**
* @return list of M_ modes
*/
/datum/admin_modal/admin_narrate/proc/resolve_modes()
var/datum/resolved = target
if(!resolved)
return list(
/datum/admin_modal/admin_narrate::M_GLOBAL,
/datum/admin_modal/admin_narrate::M_LOBBY,
)
. = list()
if(istype(resolved, /obj/overmap/entity))
. += /datum/admin_modal/admin_narrate::M_DIRECT
. += /datum/admin_modal/admin_narrate::M_OVERMAP
else if(isatom(resolved))
if(ismovable(resolved))
var/atom/movable/casted = resolved
if(casted.admin_resolve_narrate())
// compatible with direct-narrate
. += /datum/admin_modal/admin_narrate::M_DIRECT
. += /datum/admin_modal/admin_narrate::M_RANGE
. += /datum/admin_modal/admin_narrate::M_LEVEL
. += /datum/admin_modal/admin_narrate::M_SECTOR
. += /datum/admin_modal/admin_narrate::M_OVERMAP
else if(istype(resolved, /datum/map_level))
var/datum/map_level/casted = resolved
. += /datum/admin_modal/admin_narrate::M_LEVEL
. += /datum/admin_modal/admin_narrate::M_SECTOR
if(get_overmap_sector(locate(1, 1, casted.z_index)))
. += /datum/admin_modal/admin_narrate::M_OVERMAP
else if(istype(resolved, /datum/map))
. += /datum/admin_modal/admin_narrate::M_SECTOR
// TODO: check for overmap binding & add overmap if it's there
/**
* @return list of mobs, or null if invalid
*/
/datum/admin_modal/admin_narrate/proc/resolve_hearers()
// Clients are not allowed in output list; we want mobs, not clients.
. = list()
switch(mode)
if(M_GLOBAL)
// do not use client refs directly to prevent gc issues if delayed
for(var/client/C as anything in GLOB.clients)
. += C.mob
if(M_SECTOR)
var/atom/resolved = target
var/turf/maybe_target_turf = get_turf(resolved)
var/datum/map_level/maybe_target_map_level
if(maybe_target_turf)
maybe_target_map_level = SSmapping.ordered_levels[maybe_target_turf.z]
if(!maybe_target_map_level)
return null
for(var/client/C as anything in GLOB.clients)
var/turf/maybe_turf = get_turf(C.mob)
if(maybe_turf)
var/datum/map_level/maybe_map_level = SSmapping.ordered_levels[maybe_turf.z]
if(maybe_map_level?.parent_map == maybe_target_map_level.parent_map)
. += C.mob
if(M_OVERMAP)
var/datum/resolved = target
var/obj/overmap/entity/resolved_entity
if(istype(resolved, /obj/overmap/entity))
resolved_entity = resolved
else if(istype(resolved, /atom))
resolved_entity = get_overmap_sector(get_turf(resolved))
else if(istype(resolved, /datum/map_level))
var/datum/map_level/casted = resolved
resolved_entity = get_overmap_sector(locate(1, 1, casted.z_index))
// TODO: do something
else if(istype(resolved, /datum/map))
pass()
// TODO: do something
if(!resolved_entity)
return null
for(var/client/C as anything in GLOB.clients)
var/turf/maybe_turf = get_turf(C.mob)
var/obj/overmap/entity/maybe_entity = get_overmap_sector(maybe_turf)
if(maybe_entity == resolved_entity)
. += C.mob
if(M_LEVEL)
var/datum/resolved = target
var/datum/map_level/resolved_level
if(istype(resolved, /atom))
var/turf/maybe_turf = get_turf(resolved)
if(maybe_turf)
resolved_level = SSmapping.ordered_levels[maybe_turf.z]
else if(istype(resolved, /datum/map_level))
resolved_level = resolved
for(var/client/C as anything in GLOB.clients)
var/turf/maybe_turf = get_turf(C.mob)
if(maybe_turf?.z == resolved_level.z_index)
. += C.mob
if(M_RANGE)
var/atom/resolved = target
if(!istype(resolved))
return null
else
if(use_los)
for(var/mob/maybe_viewing in viewers(min(MAX_LOS_RANGE, use_range), resolved))
if(!maybe_viewing.client)
continue
. += maybe_viewing
else
var/our_z = get_z(resolved)
for(var/client/C as anything in GLOB.clients)
var/mob/maybe_viewing = C.mob
if((get_z(maybe_viewing) != our_z) || (get_dist(maybe_viewing, resolved) > use_range))
continue
if(!maybe_viewing.client)
continue
. += maybe_viewing
if(M_DIRECT)
var/atom/movable/resolved = target
if(!istype(resolved))
return null
. += resolved.admin_resolve_narrate()
if(M_LOBBY)
// do not use client refs directly to prevent gc issues if delayed
for(var/client/C as anything in GLOB.clients)
if(isnewplayer(C.mob))
. += C.mob
/datum/admin_modal/admin_narrate/proc/get_target_data()
var/datum/resolved = target
if(!is_target_valid(resolved, mode))
return null
var/turf/maybe_turf
var/datum/map_level/maybe_level
var/datum/map/maybe_map
var/obj/overmap/entity/maybe_entity
if(istype(resolved, /obj/overmap/entity))
maybe_entity = resolved
else if(isatom(resolved))
maybe_turf = get_turf(resolved)
else if(istype(resolved, /datum/map_level))
maybe_level = resolved
else if(istype(resolved, /datum/map))
maybe_map = resolved
if(maybe_turf)
maybe_level = SSmapping.ordered_levels[maybe_turf.z]
if(maybe_level)
maybe_map = maybe_level.parent_map
maybe_entity = get_overmap_sector(maybe_turf)
. = list()
if(maybe_turf)
.["coords"] = list(maybe_turf.x, maybe_turf.y, maybe_turf.z)
if(maybe_level)
.["level"] = list("index" = maybe_level.z_index, "name" = maybe_level.name)
if(maybe_map)
.["sector"] = list("name" = maybe_map.name)
if(maybe_entity)
.["overmap"] = list(
"name" = maybe_entity.name,
"x" = maybe_entity.x,
"y" = maybe_entity.y,
"map" = maybe_entity.overmap?.name,
)
/datum/admin_modal/admin_narrate/proc/is_target_valid(datum/target, mode)
switch(mode)
if(M_GLOBAL)
return TRUE
if(M_LOBBY)
return TRUE
if(M_SECTOR)
return isturf(target) || ismovable(target) || istype(target, /datum/map_level) || istype(target, /datum/map)
if(M_OVERMAP)
return isturf(target) || ismovable(target) || istype(target, /datum/map_level) || istype(target, /datum/map)
if(M_LEVEL)
return isturf(target) || ismovable(target) || istype(target, /datum/map_level)
if(M_RANGE)
return isturf(target) || ismovable(target)
if(M_DIRECT)
var/atom/movable/casted = target
return istype(casted) && casted.admin_resolve_narrate()
/datum/admin_modal/admin_narrate/proc/narrate()
if(!length(unsafe_raw_html_to_send))
return
var/list/mob/targets = resolve_hearers()
var/emit = unsafe_raw_html_to_send
var/list/view_target_to_list = list()
for(var/mob/viewing in targets)
view_target_to_list += "[key_name(viewing)]"
tim_sort(view_target_to_list, /proc/cmp_text_asc)
var/view_target_list = jointext(view_target_to_list, ", ")
message_admins("[key_name(owner.owner.mob)] sent a [SPAN_TOOLTIP("[html_encode(emit)]", "global narrate")] to [SPAN_TOOLTIP("[view_target_list]", "[length(targets)] target(s)")].")
log_admin("[key_name(owner.owner.mob)] sent a global narrate to [length(targets)] targets; VIEWERS: '[view_target_list]'', TEXT: '[emit]'")
for(var/mob/viewing in targets)
to_chat(viewing, emit)
/datum/admin_modal/admin_narrate/ui_act(action, list/params, datum/tgui/ui)
. = ..()
if(.)
return
switch(action)
if("setOutput")
unsafe_raw_html_to_send = params["target"]
return TRUE
if("setRange")
use_range = params["target"]
if(use_los)
use_range = clamp(use_range, 0, MAX_LOS_RANGE)
else
use_range = clamp(use_range, 0, MAX_ANY_RANGE)
return TRUE
if("setMode")
mode = params["mode"]
return TRUE
if("setLos")
use_los = !!params["target"]
if(use_los)
use_range = clamp(use_range, 0, MAX_LOS_RANGE)
else
use_range = clamp(use_range, 0, MAX_ANY_RANGE)
return TRUE
if("narrate")
if(params["html"])
unsafe_raw_html_to_send = params["html"]
narrate()
qdel(src)
return TRUE
if("cancel")
qdel(src)
return TRUE
if("preview")
var/emit = params["html"]
var/list/mob/targets = resolve_hearers()
var/list/rendered_viewers_list = list()
for(var/mob/target as anything in targets)
rendered_viewers_list += "[target.name][target.real_name != target.name ? " ([target.real_name])" : ""]"
tim_sort(rendered_viewers_list, /proc/cmp_text_asc)
var/rendered_viewers = jointext(rendered_viewers_list, "<br>")
var/list/html = list(
"<hr>",
SPAN_BLOCKQUOTE(emit, null),
"<hr>",
"<center>[SPAN_ADMIN("^^^ Narrate Preview; [length(rendered_viewers) ? SPAN_TOOLTIP(rendered_viewers, "Viewers..."): "No Viewers!"] ^^^")]</center>",
)
to_chat(owner.owner, jointext(html, ""))
return TRUE
/datum/admin_modal/admin_narrate/ui_interact(mob/user, datum/tgui/ui, datum/tgui/parent_ui)
ui = SStgui.try_update_ui(user, src, ui)
if(!ui)
ui = new(user, src, "admin_modal/AdminNarrate")
ui.open()
/datum/admin_modal/admin_narrate/ui_static_data(mob/user, datum/tgui/ui)
. = ..()
// UI-authoritative, so this is only even sent to server
// to keep the UI persistent across reconnects.
.["rawHtml"] = unsafe_raw_html_to_send
.["visualColor"] = narrate_visual_color
.["possibleModes"] = resolve_modes()
/datum/admin_modal/admin_narrate/ui_data(mob/user, datum/tgui/ui)
. = ..()
// This however needs to update
.["target"] = get_target_data()
.["mode"] = mode
.["useLos"] = use_los
.["useRange"] = use_range
.["maxRangeLos"] = MAX_LOS_RANGE
.["maxRangeAny"] = MAX_ANY_RANGE

View File

@@ -0,0 +1,45 @@
//* This file is explicitly licensed under the MIT license. *//
//* Copyright (c) 2024 Citadel Station Developers *//
GLOBAL_REAL_PROTECT(admin_verbs)
GLOBAL_REAL_LIST(admin_verb_descriptors) = zz__prepare_admin_verb_descriptors()
/proc/zz__prepare_admin_verb_descriptors()
. = list()
for(var/datum/admin_verb_descriptor/path as anything in subtypesof(/datum/admin_verb_descriptor))
. += new path
/**
* Describes an admin verb.
*
* When admin verbs should be used:
* * Core debug functions (VV, SDQL, similar). These should **never** be abstracted to complex
* systems like TGUI, as we need thse to debug said complex systems!
* * Anything that invokes on right click
* * Anything that is very simple to invoke ('spawn') and is used enough that removing it
* for a panel makes no sense as it would make the lives of people using the in-game
* command line worse!
*/
/datum/admin_verb_descriptor
/// our unique ID; this is based on verb path!
var/id
/// our verb path
var/verb_path
/// the reflection path to read from
var/reflection_path
/// required rights flags
var/required_rights
//* detected from verb *//
/// our name
var/name
/// our desc
var/desc
/datum/admin_verb_descriptor/New()
if(!verb_path)
return
var/procpath/cast_verb = reflection_path
name = cast_verb.name
desc = cast_verb.desc

View File

@@ -51,10 +51,6 @@ var/list/admin_verbs_admin = list(
/client/proc/jumptoturf, //allows us to jump to a specific turf,
/client/proc/admin_call_shuttle, //allows us to call the emergency shuttle,
/client/proc/admin_cancel_shuttle, //allows us to cancel the emergency shuttle, sending it back to CentCom,
/client/proc/cmd_admin_direct_narrate, //send text directly to a player with no padding. Useful for narratives and fluff-text,
/client/proc/cmd_admin_local_narrate,
/client/proc/cmd_admin_world_narrate,
/client/proc/cmd_admin_z_narrate, //sends text to all players on a z-level.When Global is too much
/client/proc/cmd_admin_create_centcom_report,
/client/proc/check_words, //displays cult-words,
/client/proc/check_ai_laws, //shows AI and borg laws,
@@ -282,10 +278,6 @@ var/list/admin_verbs_hideable = list(
/datum/admins/proc/access_news_network,
/client/proc/admin_call_shuttle,
/client/proc/admin_cancel_shuttle,
/client/proc/cmd_admin_direct_narrate,
/client/proc/cmd_admin_local_narrate,
/client/proc/cmd_admin_world_narrate,
/client/proc/cmd_admin_z_narrate,
/client/proc/check_words,
/client/proc/play_local_sound,
/client/proc/play_sound,
@@ -364,7 +356,6 @@ var/list/admin_verbs_mod = list(
/client/proc/cmd_admin_subtle_message, //send an message to somebody as a 'voice in their head',
/client/proc/cmd_admin_icsubtle_message,
/datum/admins/proc/paralyze_mob,
/client/proc/cmd_admin_direct_narrate,
/client/proc/allow_character_respawn, // Allows a ghost to respawn ,
/datum/admins/proc/sendFax,
/client/proc/addbunkerbypass,
@@ -386,7 +377,6 @@ var/list/admin_verbs_event_manager = list(
/client/proc/aooc,
/client/proc/cmd_admin_clear_mobs,
/datum/admins/proc/paralyze_mob,
/client/proc/cmd_admin_direct_narrate,
/client/proc/allow_character_respawn,
/datum/admins/proc/sendFax,
/client/proc/respawn_character,
@@ -776,7 +766,7 @@ var/list/admin_verbs_event_manager = list(
set category = "Admin"
if(deadmin_holder)
deadmin_holder.reassociate()
deadmin_holder.associate(src)
log_admin("[src] re-admined themself.")
message_admins("[src] re-admined themself.", 1)
to_chat(src, "<span class='interface'>You now have the keys to control the planet, or atleast a small space station</span>")

View File

@@ -1574,7 +1574,7 @@
if(!check_rights(R_ADMIN)) return
var/mob/M = locate(href_list["narrateto"])
usr.client.cmd_admin_direct_narrate(M)
usr.client.holder.open_admin_modal(/datum/admin_modal/admin_narrate, M)
else if(href_list["subtlemessage"])
if(!check_rights(R_MOD,0) && !check_rights(R_ADMIN)) return

View File

@@ -0,0 +1,8 @@
/client/proc/cmd_admin_delete(atom/A as obj|mob|turf in world)
set category = "Admin"
set name = "Delete"
if(!check_rights(R_SPAWN|R_DEBUG|R_ADMIN))
return
admin_delete(A)

View File

@@ -1,5 +1,5 @@
/client/proc/Debug2()
set category = ADMIN_CATEGORY_DEBUG
set category = VERB_CATEGORY_DEBUG
set name = "Debug-Game"
set desc = "Toggles game debugging."
@@ -118,7 +118,7 @@
return types[key]
/client/proc/cmd_del_all(object as text)
set category = ADMIN_CATEGORY_DEBUG
set category = VERB_CATEGORY_DEBUG
set name = "Del-All"
set desc = "Delete all datums with the specified type."
@@ -138,7 +138,7 @@
message_admins("[key_name_admin(src)] has deleted all ([counter]) instances of [type_to_del].")
/client/proc/cmd_del_all_force(object as text)
set category = ADMIN_CATEGORY_DEBUG
set category = VERB_CATEGORY_DEBUG
set name = "Force-Del-All"
set desc = "Forcibly delete all datums with the specified type."
@@ -158,7 +158,7 @@
message_admins("[key_name_admin(src)] has force-deleted all ([counter]) instances of [type_to_del].")
/client/proc/cmd_del_all_hard(object as text)
set category = ADMIN_CATEGORY_DEBUG
set category = VERB_CATEGORY_DEBUG
set name = "Hard-Del-All"
set desc = "Hard delete all datums with the specified type."
@@ -203,7 +203,7 @@
message_admins("[key_name_admin(src)] has hard deleted all ([counter]) instances of [type_to_del].")
/client/proc/cmd_debug_make_powernets()
set category = ADMIN_CATEGORY_DEBUG
set category = VERB_CATEGORY_DEBUG
set name = "Make Powernets"
set desc = "Regenerates all powernets for all cables."
@@ -215,7 +215,7 @@
feedback_add_details("admin_verb","MPWN") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc!
/client/proc/cmd_admin_grantfullaccess(var/mob/M in GLOB.mob_list)
set category = ADMIN_CATEGORY_DEBUG
set category = VERB_CATEGORY_DEBUG
set name = "Grant Full Access"
set desc = "Grant full access to a mob."
@@ -250,7 +250,7 @@
message_admins(SPAN_ADMINNOTICE("[key_name_admin(usr)] has granted [M.key] full access."))
/client/proc/cmd_debug_mob_lists()
set category = ADMIN_CATEGORY_DEBUG
set category = VERB_CATEGORY_DEBUG
set name = "Debug Mob Lists"
set desc = "For when you just gotta know"
@@ -274,7 +274,7 @@
to_chat(usr, jointext(GLOB.clients,","), confidential = TRUE)
/client/proc/cmd_display_del_log()
set category = ADMIN_CATEGORY_DEBUG
set category = VERB_CATEGORY_DEBUG
set name = "Display del() Log"
set desc = "Display del's log of everything that's passed through it."
@@ -307,7 +307,7 @@
browser.open()
/client/proc/cmd_display_overlay_log()
set category = ADMIN_CATEGORY_DEBUG
set category = VERB_CATEGORY_DEBUG
set name = "Display overlay Log"
set desc = "Display SSoverlays log of everything that's passed through it."
@@ -316,7 +316,7 @@
render_stats(SSoverlays.stats, src)
/client/proc/cmd_display_init_log()
set category = ADMIN_CATEGORY_DEBUG
set category = VERB_CATEGORY_DEBUG
set name = "Display Initialize() Log"
set desc = "Displays a list of things that didn't handle Initialize() properly"
@@ -327,7 +327,7 @@
browser.open()
/datum/admins/proc/view_runtimes()
set category = ADMIN_CATEGORY_DEBUG
set category = VERB_CATEGORY_DEBUG
set name = "View Runtimes"
set desc = "Open the Runtime Viewer"
@@ -345,7 +345,7 @@
alert(src, "[warning]. Proceed with caution. If you really need to see the runtimes, download the runtime log and view it in a text editor.", "HEED THIS WARNING CAREFULLY MORTAL")
/client/proc/check_timer_sources()
set category = ADMIN_CATEGORY_DEBUG
set category = VERB_CATEGORY_DEBUG
set name = "Check Timer Sources"
set desc = "Checks the sources of running timers."
@@ -365,7 +365,7 @@
browser.open()
/client/proc/toggle_browser_inspect()
set category = ADMIN_CATEGORY_DEBUG
set category = VERB_CATEGORY_DEBUG
set name = "Toggle Browser Inspect"
set desc = "Toggle browser debugging via inspect"

View File

@@ -0,0 +1,5 @@
//* This file is explicitly licensed under the MIT license. *//
//* Copyright (c) 2025 Citadel Station Developers *//
ADMIN_VERB_DEF(narrate, R_ADMIN, "Narrate", "Perform narration.", VERB_CATEGORY_GAME, atom/target as null|obj|mob|turf in world)
caller.holder.open_admin_modal(/datum/admin_modal/admin_narrate, target)

View File

@@ -0,0 +1,95 @@
//* This file is explicitly licensed under the MIT license. *//
//* Copyright (c) 2025 Citadel Station Developers *//
ADMIN_VERB_DEF(narrate_quick, R_ADMIN, "Narrate (Quick)", "Perform narration.", VERB_CATEGORY_GAME, atom/target as null|obj|mob|turf in world)
var/use_global
var/datum/weakref/use_viewers
var/datum/weakref/use_overmap
var/datum/weakref/use_direct
var/target_name_descriptor = "ERROR"
var/target_long_descriptor = "ERROR"
if(isnull(target))
use_global = TRUE
target_name_descriptor = "(Everyone)"
target_long_descriptor = "Narrate to the entire server, even those in lobby."
else if(istype(target, /obj/overmap/entity))
use_overmap = WEAKREF(target)
target_name_descriptor = "([target])"
target_long_descriptor = "Narrate to everyone aboard / present on \the [target]."
else if(isturf(target))
use_viewers = WEAKREF(target)
target_name_descriptor = "(viewing [target] @ [target.x], [target.y], [target.y])"
target_long_descriptor = "Narrate to everyone who can view the turf ([target]) at [target.x], [target.y], [target.z]."
else if(ismovable(target))
var/atom/movable/target_movable = target
if(target_movable.admin_resolve_narrate())
use_direct = WEAKREF(target_movable)
target_name_descriptor = "([target])"
target_long_descriptor = "Narrate to everyone inside / the person at / the target of '[target]' (currently at \the [get_area(target)])."
else
use_viewers = WEAKREF(target_movable)
target_name_descriptor = "(viewing [target])"
target_long_descriptor = "Narrate to everyone who can see '[target]' (currently at \the [get_area(target)])."
var/emit = tgui_input_text(caller, target_long_descriptor, "Narrate to [target_name_descriptor]", "", 65535, TRUE, FALSE)
if(!emit)
return
var/list/mob/targets = list()
var/reject
if(use_global)
for(var/client/C as anything in GLOB.clients)
targets += C.mob
else if(use_viewers)
var/atom/resolved = use_viewers.resolve()
if(!istype(resolved))
reject = "use_viewers failed atom resolution; this is a bug."
else
for(var/mob/maybe_viewing in viewers(35, resolved))
if(!maybe_viewing.client)
continue
targets += maybe_viewing
else if(use_overmap)
var/obj/overmap/entity/resolved = use_viewers.resolve()
if(!istype(resolved))
reject = "use_overmap failed entity resolution; this is a bug."
else
// sigh make this better later
for(var/client/C as anything in GLOB.clients)
if(get_overmap_sector(C.mob) == resolved)
targets += C.mob
else if(use_direct)
var/atom/movable/resolved = use_viewers.resolve()
if(!istype(resolved))
reject = "use_direct failed target resolution; this is a bug."
else
targets = resolved.admin_resolve_narrate()
else
reject = "no narrate mode resolved; this is a bug."
if(!reject && !length(targets))
reject = "no targets in range; skipping narrate sequence."
if(reject)
var/list/html = list(
"<hr>",
SPAN_BLOCKQUOTE(emit, null),
"<hr>",
"<center><span style='font-weight: bold; color: red;'>^^^ ERROR: The above was not sent; [reject] ^^^</span></center>",
)
to_chat(caller, jointext(html, ""))
return
var/list/view_target_to_list = list()
for(var/mob/viewing in targets)
view_target_to_list += "[key_name(viewing)]"
var/view_target_list = jointext(view_target_to_list, ", ")
message_admins("[key_name(caller)] sent a [SPAN_TOOLTIP("[html_encode(emit)]", "global narrate")] to [SPAN_TOOLTIP("[view_target_list]", "[length(targets)] target(s)")].")
log_admin("[key_name(caller)] sent a global narrate to [length(targets)] targets; VIEWERS: '[view_target_list]'', TEXT: '[emit]'")
for(var/mob/viewing in targets)
to_chat(viewing, emit)

View File

@@ -101,86 +101,6 @@
admin_ticket_log(M, msg)
feedback_add_details("admin_verb","SMS") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc!
/client/proc/cmd_admin_world_narrate()
set category = "Special Verbs"
set name = "Global Narrate"
if(!check_rights(R_ADMIN))
return
var/msg = input("Message:", "Enter the text you wish to appear to everyone:") as text|null
if (!msg)
return
to_chat(world, "[msg]")
log_admin("GlobalNarrate: [key_name(usr)] : [msg]")
message_admins("<span class='adminnotice'>[key_name_admin(usr)] Sent a global narrate</span>")
// SSblackbox.record_feedback("tally", "admin_verb", 1, "Global Narrate") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc!
/client/proc/cmd_admin_direct_narrate(mob/M)
set category = "Special Verbs"
set name = "Direct Narrate"
if(!check_rights(R_ADMIN))
return
if(!M)
M = input("Direct narrate to whom?", "Active Players") as null|anything in GLOB.player_list
if(!M)
return
var/msg = input("Message:", "Enter the text you wish to appear to your target:") as text|null
if( !msg )
return
to_chat(M, msg)
log_admin("DirectNarrate: [key_name(usr)] to ([M.name]/[M.key]): [msg]")
msg = "<span class='adminnotice'><b> DirectNarrate: [key_name(usr)] to ([M.name]/[M.key]):</b> [msg]<BR></span>"
message_admins(msg)
admin_ticket_log(M, msg)
// SSblackbox.record_feedback("tally", "admin_verb", 1, "Direct Narrate") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc!
/client/proc/cmd_admin_local_narrate(atom/A)
set category = "Special Verbs"
set name = "Local Narrate"
if(!check_rights(R_ADMIN))
return
if(!A)
return
var/range = input("Range:", "Narrate to mobs within how many tiles:", 7) as num|null
if(!range)
return
var/msg = input("Message:", "Enter the text you wish to appear to everyone within view:") as text|null
if (!msg)
return
for(var/mob/M in view(range,A))
to_chat(M, msg)
log_admin("LocalNarrate: [key_name(usr)] at [AREACOORD(A)]: [msg]")
message_admins("<span class='adminnotice'><b> LocalNarrate: [key_name_admin(usr)] at [ADMIN_COORDJMP(A)]:</b> [msg]<BR></span>")
// SSblackbox.record_feedback("tally", "admin_verb", 1, "Local Narrate") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc!
/client/proc/cmd_admin_z_narrate()
set category = "Special Verbs"
set name = "Z Narrate"
if(!check_rights(R_ADMIN))
return
var/msg = input("Enter the text you wish to show an entire Z level:", "Z Narrate:") as text|null
if (!msg)
return
for(var/mob/M in range(192)) //Yes this is lazy
to_chat(M, msg)
log_admin("ZNarrate: [key_name(usr)] at [ADMIN_COORDJMP(usr)]: [msg]")
message_admins("<span class='adminnotice'><b> ZNarrate: [key_name_admin(usr)] at [ADMIN_COORDJMP(usr)]:</b> [msg]<BR></span>")
/client/proc/cmd_admin_godmode(mob/M as mob in GLOB.mob_list)
set category = "Special Verbs"
@@ -632,15 +552,6 @@ Traitors and the like can also be revived with the previous role mostly intact.
message_admins("[key_name_admin(src)] has created a command report", 1)
feedback_add_details("admin_verb","CCR") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc!
/client/proc/cmd_admin_delete(atom/A as obj|mob|turf in world)
set category = "Admin"
set name = "Delete"
if(!check_rights(R_SPAWN|R_DEBUG|R_ADMIN))
return
admin_delete(A)
/client/proc/cmd_admin_list_open_jobs()
set category = "Admin"
set name = "List free slots"

View File

@@ -173,9 +173,6 @@
/client/New(TopicData)
//* pre-connect-ish *//
// Byond only populates whether or not you can profile at connect. You have to give someone this
// before their client loads/whatever. This cannot be behind a spawn(). We will remove it from non-admins later.
world.SetConfig("APP/admin", ckey, "role=admin")
// Block client.Topic() calls from connect.
TopicData = null
// Kick invalid connections.
@@ -238,39 +235,20 @@
action_holder = new /datum/action_holder/client_actor(src)
action_drawer.register_holder(action_holder)
//* Setup admin tooling
//* Setup admin tooling *//
// Notify tickets they logged in
GLOB.ahelp_tickets.ClientLogin(src)
// var/connecting_admin = FALSE //because de-admined admins connecting should be treated like admins.
//Admin Authorisation
holder = admin_datums[ckey]
var/debug_tools_allowed = FALSE
if(holder)
GLOB.admins |= src
holder.owner = src
// connecting_admin = TRUE
//if(check_rights_for(src, R_DEBUG))
if(R_DEBUG & holder?.rights) //same wiht this, check_rights when?
debug_tools_allowed = TRUE
/*
else if(GLOB.deadmins[ckey])
add_verb(src, /client/proc/readmin)
connecting_admin = TRUE
*/
// if(CONFIG_GET(flag/enable_localhost_rank) && !connecting_admin)
if(is_localhost() && CONFIG_GET(flag/enable_localhost_rank))
// Give them admin if they're an admin
// TODO: Maybe don't do it if they're deadminned..
var/datum/admins/maybe_holder = admin_datums[ckey]
maybe_holder?.associate(src)
//! TODO: This is shitcode, fix it.
if(is_localhost() && CONFIG_GET(flag/enable_localhost_rank) && !holder)
holder = new /datum/admins("!localhost!", ALL, ckey)
holder.owner = src
GLOB.admins |= src
//admins |= src // this makes them not have admin. what the fuck??
// holder.associate(ckey)
// connecting_admin = TRUE
//CITADEL EDIT
//if(check_rights_for(src, R_DEBUG)) //check if autoadmin gave us it
if(R_DEBUG & holder?.rights) //this is absolutely horrid
debug_tools_allowed = TRUE
if(!debug_tools_allowed)
world.SetConfig("APP/admin", ckey, null)
//END CITADEL EDIT
holder.associate(src)
//! END
// todo: refactor and hoist
//preferences datum - also holds some persistent data for the client (because we may as well keep these datums to a minimum)
prefs = GLOB.preferences_datums[ckey]
@@ -417,9 +395,8 @@
prefs.client = null
prefs = null
SSserver_maint.UpdateHubStatus()
if(holder)
holder.owner = null
GLOB.admins -= src //delete them on the managed one too
holder?.disassociate()
//* Cleanup rendering *//
if(using_perspective)

View File

@@ -0,0 +1,7 @@
//* This file is explicitly licensed under the MIT license. *//
//* Copyright (c) 2024 Citadel Station Developers *//
/datum/metric/nested_numerical/admin_verb_invocation
id = "admin-verb-invocation"
name = "Admin - Verb Invocation"
category = METRIC_CATEGORY_ADMIN

View File

@@ -1,3 +1,6 @@
//* This file is explicitly licensed under the MIT license. *//
//* Copyright (c) 2024 Citadel Station Developers *//
/datum/metric/nested_numerical/subsystem_init_time
id = "subsystem-init-time"
name = "Init Time - Subsystem"

View File

@@ -0,0 +1,5 @@
//* This file is explicitly licensed under the MIT license. *//
//* Copyright (c) 2025 Citadel Station Developers *//
/mob/admin_resolve_narrate()
return list(src)

View File

@@ -114,6 +114,24 @@
return 0
return y - overmap.lower_left_y + 1
/**
* Get floating point tile X where being on a tile is considered being at the center of it.
*/
/obj/overmap/entity/proc/get_tile_x_f()
if(!overmap)
return 0
var/center = bound_x + bound_width * 0.5
return x - overmap.lower_left_x + 1 + (center - (WORLD_ICON_SIZE * 0.5)) / WORLD_ICON_SIZE
/**
* Get floating point tile Y where being on a tile is considered being at the center of it.
*/
/obj/overmap/entity/proc/get_tile_y_f()
if(!overmap)
return 0
var/center = bound_y + bound_height * 0.5
return y - overmap.lower_left_y + 1 + (center - (WORLD_ICON_SIZE * 0.5)) / WORLD_ICON_SIZE
/**
* gets our movement (non-angular) speed in overmaps units per second
*/

View File

@@ -0,0 +1,15 @@
//* This file is explicitly licensed under the MIT license. *//
//* Copyright (c) 2024 Citadel Station Developers *//
//* UI State *//
GLOBAL_DATUM_INIT(ui_admin_modal_state, /datum/ui_state/admin_modal_state, new)
/**
* Admin modal state. **Only valid on /datum/admin_modal.**
*/
VV_PROTECT_READONLY(/datum/admin_modal_state)
/datum/ui_state/admin_modal_state
/datum/ui_state/admin_modal_state/can_use_topic(datum/admin_modal/src_object, mob/user)
return src_object.owner.owner == user.client ? UI_INTERACTIVE : UI_CLOSE

View File

@@ -0,0 +1,5 @@
//* This file is explicitly licensed under the MIT license. *//
//* Copyright (c) 2025 Citadel Station Developers *//
/obj/vehicle/admin_resolve_narrate()
return occupants

View File

@@ -0,0 +1,266 @@
/**
* @file
* @license MIT
*/
import { Component, Fragment } from "react";
import { Button, LabeledList, NumberInput, Section, Stack, TextArea } from "tgui-core/components";
import { BooleanLike } from "tgui-core/react";
import { useBackend } from "../../backend";
import { Window } from "../../layouts";
import { ByondColorString } from "../common/Color";
interface AdminNarrateData {
visualColor: ByondColorString;
possibleModes: AdminNarrateMode[];
mode: AdminNarrateMode;
rawHtml: string;
target: AdminNarrateTarget | null;
useLos: BooleanLike;
useRange: number;
maxRangeLos: number;
maxRangeAny: number;
}
interface AdminNarrateTarget {
coords: [number, number, number] | null;
level: {
index: number;
name: string;
} | null;
sector: {
name: string;
} | null;
overmap: {
name: string;
x: number;
y: number;
map: string;
} | null;
}
enum AdminNarrateMode {
Global = "global",
Sector = "sector",
Overmap = "overmap",
Level = "level",
Range = "range",
Direct = "direct",
Lobby = "lobby",
}
const MODES_REQUIRING_TARGET: AdminNarrateMode[] = [
AdminNarrateMode.Direct,
AdminNarrateMode.Range,
AdminNarrateMode.Overmap,
AdminNarrateMode.Sector,
AdminNarrateMode.Level,
];
const MODES_REQUIRING_RANGE: AdminNarrateMode[] = [
AdminNarrateMode.Range,
AdminNarrateMode.Overmap,
];
const ADMIN_NARRATE_MODE_NAMES: Record<AdminNarrateMode, string> = {
[AdminNarrateMode.Global]: "Global",
[AdminNarrateMode.Sector]: "Sector",
[AdminNarrateMode.Overmap]: "Overmap",
[AdminNarrateMode.Level]: "Level",
[AdminNarrateMode.Range]: "Range",
[AdminNarrateMode.Direct]: "Direct",
[AdminNarrateMode.Lobby]: "Lobby",
};
const ADMIN_NARRATE_MODE_DESCS: Record<AdminNarrateMode, string> = {
[AdminNarrateMode.Global]: "Send to everyone in the server, including those sitting in the lobby.",
[AdminNarrateMode.Sector]: "Send to everyone in the logical map, whether that map may be a planet or a ship. This includes anyone sitting in its z-levels but not on the map, e.g. visiting shuttles.",
[AdminNarrateMode.Overmap]: "Send to target and nearby overmap entities. Range is measured in <b>tiles</b>, not in overmap distance!",
[AdminNarrateMode.Level]: "Send to everyone in the z-level, regardless of line of sight.",
[AdminNarrateMode.Range]: "Send to everyone within range. If 'LoS' is enabled, this will check for viewers. Viewer check is done via BYOND 'viewers()', entirely ignoring things like blindness, darksight, etc.",
[AdminNarrateMode.Direct]: "Send directly to an entity. What this does depends on the entity; it generally will target the mob itself if it's a mob, everyone inside a vehicle, the overmap level if it's an overmap entity, etc. This is a logical 'contains'. Note: Vore bellies are excluded from this (I hate that I have to specify it).",
[AdminNarrateMode.Lobby]: "Send to everyone in the lobby.",
};
interface AdminNarrateState {
emitHtml: string | null;
edited: boolean;
}
export class AdminNarrate extends Component<{}, AdminNarrateState> {
timeoutRef: any;
state: AdminNarrateState = {
emitHtml: null,
edited: false,
};
componentDidMount(): void {
this.timeoutRef = setInterval(() => {
if (this.state.edited) {
this.setState((old) => ({ ...old, edited: false }));
const { act } = useBackend<AdminNarrateData>();
act("setOutput", { target: this.state.emitHtml });
}
}, 2500);
}
componentWillUnmount(): void {
clearTimeout(this.timeoutRef);
}
render() {
const { act, data } = useBackend<AdminNarrateData>();
return (
<Window width={900} height={500} title="Narrate">
<Window.Content>
<Stack fill vertical>
<Stack.Item grow={1}>
<Stack fill>
<Stack.Item width="70%">
<Section title={(
// eslint-disable-next-line react/no-danger
<div dangerouslySetInnerHTML={{ __html: "Content - <b>Shift-Enter</b> to insert a Line-Break!" }} />
)} fill>
<TextArea width="100%" height="100%"
value={this.state.emitHtml || ""}
onChange={(val) =>
this.setState((old) =>
({ ...old, edited: true, emitHtml: val }))} />
</Section>
</Stack.Item>
<Stack.Item width="30%">
<Stack vertical fill>
<Stack.Item grow={1}>
<Section title="Mode" fill>
<Stack fill vertical>
{data.possibleModes.map((mode) => {
let selected = mode === data.mode;
return (
<Stack.Item key={mode}>
<Stack>
<Stack.Item>
<Button icon="question" tooltip={ADMIN_NARRATE_MODE_DESCS[mode]} />
</Stack.Item>
<Stack.Item grow>
<Button
style={{ textAlign: "left" }}
fluid color={selected ? "" : "transparent"} selected={selected}
onClick={() => act('setMode', { mode: mode })}>
{ADMIN_NARRATE_MODE_NAMES[mode]}
</Button>
</Stack.Item>
</Stack>
</Stack.Item>
);
})}
</Stack>
</Section>
</Stack.Item>
<Stack.Item grow={1}>
<Section title="Settings" fill>
<Stack vertical fill>
{MODES_REQUIRING_RANGE.includes(data.mode) && (
<>
<Stack.Item>
<Stack>
<Stack.Item grow={1}>
Use LoS
</Stack.Item>
<Stack.Item>
<Button icon="question" tooltip="Whether the narrate will check line of sight. If enabled, only people who can see the entity can view it; otherwise everyone in Chebyshev (square-radius) distance can." />
</Stack.Item>
<Stack.Item grow={1}>
<Stack fill>
<Stack.Item grow={1}>
<Button fluid selected={data.useLos} onClick={() => act('setLos', { target: true })}>Yes</Button>
</Stack.Item>
<Stack.Item grow={1}>
<Button fluid selected={!data.useLos} onClick={() => act('setLos', { target: false })}>No</Button>
</Stack.Item>
</Stack>
</Stack.Item>
</Stack>
</Stack.Item>
<Stack.Item>
<Stack>
<Stack.Item grow={1}>
Range
</Stack.Item>
<Stack.Item>
<Button icon="question" tooltip="Distance, in tiles, to broadcast. Fractional tiles are allowed in overmaps mode." />
</Stack.Item>
<Stack.Item grow={1}>
<NumberInput width="100%"
step={data.mode === AdminNarrateMode.Overmap ? 0.01 : 1}
value={data.useRange}
minValue={0}
maxValue={data.maxRangeAny}
onChange={(val) => act('setRange', { target: val })} />
</Stack.Item>
</Stack>
</Stack.Item>
</>
)}
</Stack>
</Section>
</Stack.Item>
<Stack.Item grow={1}>
<Section title="Target" fill>
<LabeledList>
{data.target?.coords && (
<LabeledList.Item label="Coords">{data.target.coords[0]}, {data.target.coords[1]}, {data.target.coords[2]}</LabeledList.Item>
)}
{data.target?.level && (
<LabeledList.Item label="Level">{data.target.level.name} - Z{data.target.level.index}</LabeledList.Item>
)}
{data.target?.sector && (
<LabeledList.Item label="Sector">{data.target.sector.name}</LabeledList.Item>
)}
{data.target?.overmap && (
<LabeledList.Item label="Overmap">{data.target.overmap.name} @ {data.target.overmap.x} - {data.target.overmap.y} ({data.target.overmap.map})</LabeledList.Item>
)}
</LabeledList>
</Section>
</Stack.Item>
</Stack>
</Stack.Item>
</Stack>
</Stack.Item>
<Stack.Item>
<Section fill>
<Stack fill>
<Stack.Item grow={1}>
<Button.Confirm fluid
style={{ textAlign: "center" }}
color="transparent"
onClick={() => act("cancel")}>
Cancel
</Button.Confirm>
</Stack.Item>
<Stack.Item grow={1}>
<Button.Confirm fluid
style={{ textAlign: "center" }}
color="transparent"
onClick={() => act("preview", { html: this.state.emitHtml })}>
Preview
</Button.Confirm>
</Stack.Item>
<Stack.Item grow={1}>
<Button.Confirm fluid
style={{ textAlign: "center" }}
color="transparent"
onClick={() => act("narrate", { html: this.state.emitHtml })}>
Send
</Button.Confirm>
</Stack.Item>
</Stack>
</Section>
</Stack.Item>
</Stack>
</Window.Content >
</Window >
);
}
}