mirror of
https://github.com/Citadel-Station-13/Citadel-Station-13-RP.git
synced 2026-08-21 06:36:48 +01:00
<!-- Write **BELOW** The Headers and **ABOVE** The comments else it may not be viewable. --> <!-- You can view Contributing.MD for a detailed description of the pull request process. --> ## About The Pull Request Fix OD0013 error 'rgb takes 3 to 5 arguments' May technically be an OpenDream compiler error as this presumably does work as is. But Lohikar suggested a better solution for modern byond and we like better solutions. <!-- Describe The Pull Request. Please be sure every change is documented or this can delay review and even discourage maintainers from merging your PR! --> ## Why It's Good For The Game Cleans up 1 line of dirty code. OD compatibility++ <!-- Argue for the merits of your changes and how they benefit the game, especially if they are controversial and/or far reaching. If you can't actually explain WHY what you are doing will improve the game, then it probably isn't good for the game in the first place. --> ## Changelog <!-- If your PR modifies aspects of the game that can be concretely observed by players or admins you should add a changelog. If your change does NOT meet this description, remove this section. Please note that maintainers freely reserve the right to remove and add tags should they deem it appropriate. You can attempt to finagle the system all you want, but it's best to shoot for clear communication right off the bat. --> 🆑 fix: Fix a bad use of rgb() /🆑 <!-- Both 🆑's are required for the changelog to work! You can put your name to the right of the first 🆑 if you want to overwrite your GitHub username as author ingame. --> <!-- You can use multiple of the same prefix (they're only used for the icon ingame) and delete the unneeded ones. Despite some of the tags, changelogs should generally represent how a player might be affected by the changes rather than a summary of the PR's contents. -->
362 lines
11 KiB
Plaintext
362 lines
11 KiB
Plaintext
//* 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(rand(0, 360), rand(0, 360), rand(125, 360), space = COLORSPACE_HSL)
|
|
|
|
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
|