diff --git a/_maps/map_files/generic/CentCom.dmm b/_maps/map_files/generic/CentCom.dmm index de0cba96be2f..451b655413d7 100644 --- a/_maps/map_files/generic/CentCom.dmm +++ b/_maps/map_files/generic/CentCom.dmm @@ -26,7 +26,7 @@ /turf/open/floor/plasteel/dark, /area/tdome/tdomeadmin) "afh" = ( -/obj/machinery/computer/helm{ +/obj/machinery/computer{ dir = 4 }, /obj/effect/turf_decal/industrial/warning{ @@ -1356,7 +1356,7 @@ /turf/open/floor/plasteel, /area/wizard_station) "ara" = ( -/obj/machinery/computer/helm, +/obj/machinery/computer, /turf/open/floor/plasteel, /area/wizard_station) "ard" = ( @@ -3940,7 +3940,7 @@ /turf/open/floor/mineral/titanium/blue, /area/centcom/evac) "aLP" = ( -/obj/machinery/computer/helm{ +/obj/machinery/computer{ dir = 1 }, /turf/open/floor/mineral/titanium/blue, @@ -8846,7 +8846,7 @@ }, /area/centcom) "gFU" = ( -/obj/machinery/computer/helm, +/obj/machinery/computer, /obj/effect/turf_decal/industrial/warning{ dir = 6 }, @@ -12249,7 +12249,7 @@ /turf/open/floor/plasteel/dark, /area/tdome/tdomeadmin) "nEL" = ( -/obj/machinery/computer/helm, +/obj/machinery/computer, /obj/effect/turf_decal/industrial/warning{ dir = 10 }, @@ -15797,7 +15797,7 @@ /turf/open/floor/plasteel/dark, /area/ctf) "vcL" = ( -/obj/machinery/computer/helm, +/obj/machinery/computer, /obj/effect/turf_decal/corner/transparent/bar, /obj/effect/turf_decal/corner/transparent/bar{ dir = 1 diff --git a/code/__DEFINES/dcs/signals/signals.dm b/code/__DEFINES/dcs/signals/signals.dm index 38f7d8692853..f3295b737904 100644 --- a/code/__DEFINES/dcs/signals/signals.dm +++ b/code/__DEFINES/dcs/signals/signals.dm @@ -735,3 +735,9 @@ ///sent when the access on an id is changed/updated, ensures wallets get updated once ids generate there access #define COSMIG_ACCESS_UPDATED "acces_updated" + +// Point of interest signals +/// Sent from base of /datum/controller/subsystem/points_of_interest/proc/on_poi_element_added : (atom/new_poi) +#define COMSIG_ADDED_POINT_OF_INTEREST "added_point_of_interest" +/// Sent from base of /datum/controller/subsystem/points_of_interest/proc/on_poi_element_removed : (atom/old_poi) +#define COMSIG_REMOVED_POINT_OF_INTEREST "removed_point_of_interest" diff --git a/code/__HELPERS/_lists.dm b/code/__HELPERS/_lists.dm index 435b83e29797..b231b4e944c9 100644 --- a/code/__HELPERS/_lists.dm +++ b/code/__HELPERS/_lists.dm @@ -87,6 +87,42 @@ };\ } while(FALSE) + +/** + * Custom binary search sorted insert utilising comparison procs instead of vars. + * INPUT: Object to be inserted + * LIST: List to insert object into + * TYPECONT: The typepath of the contents of the list + * COMPARE: The object to compare against, usualy the same as INPUT + * COMPARISON: The plaintext name of a proc on INPUT that takes a single argument to accept a single element from LIST and returns a positive, negative or zero number to perform a comparison. + * COMPTYPE: How should the values be compared? Either COMPARE_KEY or COMPARE_VALUE. + */ +#define BINARY_INSERT_PROC_COMPARE(INPUT, LIST, TYPECONT, COMPARE, COMPARISON, COMPTYPE) \ + do {\ + var/list/__BIN_LIST = LIST;\ + var/__BIN_CTTL = length(__BIN_LIST);\ + if(!__BIN_CTTL) {\ + __BIN_LIST += INPUT;\ + } else {\ + var/__BIN_LEFT = 1;\ + var/__BIN_RIGHT = __BIN_CTTL;\ + var/__BIN_MID = (__BIN_LEFT + __BIN_RIGHT) >> 1;\ + var ##TYPECONT/__BIN_ITEM;\ + while(__BIN_LEFT < __BIN_RIGHT) {\ + __BIN_ITEM = COMPTYPE;\ + if(__BIN_ITEM.##COMPARISON(COMPARE) <= 0) {\ + __BIN_LEFT = __BIN_MID + 1;\ + } else {\ + __BIN_RIGHT = __BIN_MID;\ + };\ + __BIN_MID = (__BIN_LEFT + __BIN_RIGHT) >> 1;\ + };\ + __BIN_ITEM = COMPTYPE;\ + __BIN_MID = __BIN_ITEM.##COMPARISON(COMPARE) > 0 ? __BIN_MID : __BIN_MID + 1;\ + __BIN_LIST.Insert(__BIN_MID, INPUT);\ + };\ + } while(FALSE) + //Returns a list in plain english as a string /proc/english_list(list/input, nothing_text = "nothing", and_text = " and ", comma_text = ", ", final_comma_text = "" ) var/total = length(input) diff --git a/code/__HELPERS/roundend.dm b/code/__HELPERS/roundend.dm index 97740a79537e..01fd964120ea 100644 --- a/code/__HELPERS/roundend.dm +++ b/code/__HELPERS/roundend.dm @@ -116,7 +116,7 @@ SSblackbox.record_feedback("associative", "antagonists", 1, antag_info) /datum/controller/subsystem/ticker/proc/record_nuke_disk_location() - var/obj/item/disk/nuclear/N = locate() in GLOB.poi_list + var/obj/item/disk/nuclear/N = locate() in SSpoints_of_interest.other_points_of_interest if(N) var/list/data = list() var/turf/T = get_turf(N) diff --git a/code/__HELPERS/unsorted.dm b/code/__HELPERS/unsorted.dm index 36764c6bae9f..d831296dbb62 100644 --- a/code/__HELPERS/unsorted.dm +++ b/code/__HELPERS/unsorted.dm @@ -294,65 +294,6 @@ Turf and target are separate in case you want to teleport some distance from a t /proc/ionnum() return "[pick("!","@","#","$","%","^","&")][pick("!","@","#","$","%","^","&","*")][pick("!","@","#","$","%","^","&","*")][pick("!","@","#","$","%","^","&","*")]" -//Returns a list of all items of interest with their name -/proc/getpois(mobs_only = FALSE, skip_mindless = FALSE, specify_dead_role = TRUE) - var/list/mobs = sortmobs() - var/list/namecounts = list() - var/list/pois = list() - for(var/mob/M in mobs) - if(skip_mindless && (!M.mind && !M.ckey)) - if(!isbot(M) && !iscameramob(M) && !ismegafauna(M)) - continue - if(M.client && M.client.holder && M.client.holder.fakekey) //stealthmins - continue - var/name = avoid_assoc_duplicate_keys(M.name, namecounts) + M.get_realname_string() - - if(M.stat == DEAD && specify_dead_role) - if(isobserver(M)) - name += " \[ghost\]" - else - name += " \[dead\]" - pois[name] = M - - if(!mobs_only) - for(var/atom/A in GLOB.poi_list) - if(!A || !A.loc) - continue - pois[avoid_assoc_duplicate_keys(A.name, namecounts)] = A - - return pois -//Orders mobs by type then by name -/proc/sortmobs() - var/list/moblist = list() - var/list/sortmob = sortNames(GLOB.mob_list) - for(var/mob/living/silicon/ai/M in sortmob) - moblist.Add(M) - for(var/mob/camera/M in sortmob) - moblist.Add(M) - for(var/mob/living/silicon/pai/M in sortmob) - moblist.Add(M) - for(var/mob/living/silicon/robot/M in sortmob) - moblist.Add(M) - for(var/mob/living/carbon/human/M in sortmob) - moblist.Add(M) - for(var/mob/living/brain/M in sortmob) - moblist.Add(M) - for(var/mob/living/carbon/alien/M in sortmob) - moblist.Add(M) - for(var/mob/dead/observer/M in sortmob) - moblist.Add(M) - for(var/mob/dead/new_player/M in sortmob) - moblist.Add(M) - for(var/mob/living/carbon/monkey/M in sortmob) - moblist.Add(M) - for(var/mob/living/simple_animal/slime/M in sortmob) - moblist.Add(M) - for(var/mob/living/simple_animal/M in sortmob) - moblist.Add(M) - for(var/mob/living/carbon/true_devil/M in sortmob) - moblist.Add(M) - return moblist - // Format a power value in W, kW, MW, or GW. /proc/DisplayPower(powerused) if(powerused < 1000) //Less than a kW @@ -384,7 +325,7 @@ Turf and target are separate in case you want to teleport some distance from a t /proc/get_mob_by_ckey(key) if(!key) return - var/list/mobs = sortmobs() + var/list/mobs = SSpoints_of_interest.get_mob_pois() for(var/mob/M in mobs) if(M.ckey == key) return M diff --git a/code/_globalvars/lists/objects.dm b/code/_globalvars/lists/objects.dm index 7ee53ec1f37b..6d08f1d1bceb 100644 --- a/code/_globalvars/lists/objects.dm +++ b/code/_globalvars/lists/objects.dm @@ -46,8 +46,6 @@ GLOBAL_LIST_EMPTY(apcs_list) GLOBAL_LIST_EMPTY(tracked_implants) /// List of implants the prisoner console can track and send inject commands too GLOBAL_LIST_EMPTY(tracked_chem_implants) -/// List of points of interest for observe/follow -GLOBAL_LIST_EMPTY(poi_list) /// List of all pinpointers. Used to change stuff they are pointing to all at once. GLOBAL_LIST_EMPTY(pinpointer_list) /// List of all zombie_infection organs, for any mass "animation" diff --git a/code/controllers/subsystem/points_of_interest.dm b/code/controllers/subsystem/points_of_interest.dm new file mode 100644 index 000000000000..b8e8b6397424 --- /dev/null +++ b/code/controllers/subsystem/points_of_interest.dm @@ -0,0 +1,225 @@ +/// Subsystem for managing all POIs. +SUBSYSTEM_DEF(points_of_interest) + name = "Points of Interest" + + flags = SS_NO_FIRE | SS_NO_INIT + + /// List of mob POIs. This list is automatically sorted. + var/list/datum/point_of_interest/mob_poi/mob_points_of_interest = list() + /// List of non-mob POIs. This list is automatically sorted. + var/list/datum/point_of_interest/other_points_of_interest = list() + /// List of all value:POI datums by their key:target refs. + var/list/datum/point_of_interest/points_of_interest_by_target_ref = list() + +/** + * Turns new_poi into a new point of interest by adding the /datum/element/point_of_interest element to it. + */ +/datum/controller/subsystem/points_of_interest/proc/make_point_of_interest(atom/new_poi) + new_poi.AddElement(/datum/element/point_of_interest) + +/** + * Stops old_poi from being a point of interest by removing the /datum/element/point_of_interest element from it. + */ +/datum/controller/subsystem/points_of_interest/proc/remove_point_of_interest(atom/old_poi) + old_poi.RemoveElement(/datum/element/point_of_interest) + +/** + * Called by [/datum/element/point_of_interest] when it gets removed from old_poi. + */ +/datum/controller/subsystem/points_of_interest/proc/on_poi_element_added(atom/new_poi) + var/datum/point_of_interest/new_poi_datum + if(ismob(new_poi)) + new_poi_datum = new /datum/point_of_interest/mob_poi(new_poi) + BINARY_INSERT_PROC_COMPARE(new_poi_datum, mob_points_of_interest, /datum/point_of_interest/mob_poi, new_poi_datum, compare_to, COMPARE_KEY) + points_of_interest_by_target_ref[REF(new_poi)] = new_poi_datum + else + new_poi_datum = new /datum/point_of_interest(new_poi) + BINARY_INSERT_PROC_COMPARE(new_poi_datum, other_points_of_interest, /datum/point_of_interest, new_poi_datum, compare_to, COMPARE_KEY) + points_of_interest_by_target_ref[REF(new_poi)] = new_poi_datum + + + SEND_SIGNAL(src, COMSIG_ADDED_POINT_OF_INTEREST, new_poi) + +/** + * Called by [/datum/element/point_of_interest] when it gets removed from old_poi. + */ +/datum/controller/subsystem/points_of_interest/proc/on_poi_element_removed(atom/old_poi) + var/poi_ref = REF(old_poi) + var/datum/point_of_interest/poi_to_remove = points_of_interest_by_target_ref[poi_ref] + + if(!poi_to_remove) + return + + if(ismob(old_poi)) + mob_points_of_interest -= poi_to_remove + else + other_points_of_interest -= poi_to_remove + + points_of_interest_by_target_ref -= poi_ref + + poi_to_remove.target = null + + SEND_SIGNAL(src, COMSIG_REMOVED_POINT_OF_INTEREST, old_poi) + +/** + * If there is a valid POI for a given reference, it returns that POI's associated atom. Otherwise, it returns null. + */ +/datum/controller/subsystem/points_of_interest/proc/get_poi_atom_by_ref(reference) + return points_of_interest_by_target_ref[reference]?.target + +/** + * Returns a list of mob POIs with names as keys and mobs as values. + * + * If multiple POIs have the same name, then avoid_assoc_duplicate_keys is used alongside used_name_list to + * tag them as Mob Name (1), Mob Name (2), Mob Name (3) etc. + * + * Arguments: + * * poi_validation_override - [OPTIONAL] Callback to a proc that takes a single argument for the POI and returns TRUE if this POI should be included. Overrides standard POI validation. + * * append_dead_role - [OPTIONAL] If TRUE, adds a ghost tag to the end of observer names and a dead tag to the end of any other mob which is not alive. + */ +/datum/controller/subsystem/points_of_interest/proc/get_mob_pois(datum/callback/poi_validation_override = null, append_dead_role = TRUE) + var/list/pois = list() + var/list/used_name_list = list() + + for(var/datum/point_of_interest/mob_poi/mob_poi as anything in mob_points_of_interest) + if(poi_validation_override) + if(!poi_validation_override.Invoke(mob_poi)) + continue + else if(!mob_poi.validate()) + continue + + var/mob/target_mob = mob_poi.target + var/name = avoid_assoc_duplicate_keys(target_mob.name, used_name_list) + target_mob.get_realname_string() + + // Add the ghost/dead tag to the end of dead mob POIs. + if(append_dead_role && target_mob.stat == DEAD) + if(isobserver(target_mob)) + name += " \[ghost\]" + else + name += " \[dead\]" + + pois[name] = target_mob + + return pois + +/** + * Returns a list of non-mob POIs with names as keys and atoms as values. + * + * If multiple POIs have the same name, then avoid_assoc_duplicate_keys is used alongside used_name_list to + * tag them as Object Name (1), Object Name (2), Object Name (3) etc. + * + * Arguments: + * * poi_validation_override - [OPTIONAL] Callback to a proc that takes a single argument for the POI and returns TRUE if this POI should be included. Overrides standard POI validation. + */ +/datum/controller/subsystem/points_of_interest/proc/get_other_pois(datum/callback/poi_validation_override = null) + var/list/pois = list() + var/list/used_name_list = list() + + for(var/datum/point_of_interest/other_poi as anything in other_points_of_interest) + if(poi_validation_override) + if(!poi_validation_override.Invoke(other_poi)) + continue + else if(!other_poi.validate()) + continue + + var/atom/target_poi = other_poi.target + + pois[avoid_assoc_duplicate_keys(target_poi.name, used_name_list)] = target_poi + + return pois + +/// Returns TRUE if potential_poi has an associated poi_datum that validates. +/datum/controller/subsystem/points_of_interest/proc/is_valid_poi(atom/potential_poi, datum/callback/poi_validation_override = null) + var/datum/point_of_interest/poi_datum = points_of_interest_by_target_ref[REF(potential_poi)] + + if(!poi_datum) + return FALSE + + if(poi_validation_override) + return poi_validation_override.Invoke(poi_datum) + + return poi_datum.validate() + +/// Simple helper datum for points of interest. +/datum/point_of_interest + /// The specific point of interest this datum references. This won't hard del as the POI element will be removed from the target when it qdels, which will clear this reference. + var/atom/target + /// The type of POI this datum references. + var/poi_type = /atom + +/datum/point_of_interest/New(poi_target) + if(!istype(poi_target, poi_type)) + CRASH("Incorrect target type provided to /datum/point_of_interest/New: Expected \[[poi_type]\]") + + target = poi_target + +/// Validates the POI. Returns TRUE if the POI has valid state, returns FALSE if the POI has invalid state. +/datum/point_of_interest/proc/validate() + // In nullspace, invalid as a POI. + if(!target.loc) + return FALSE + + return TRUE + +/// Comparison proc used to sort POIs. Override to implement logic used doing binary sort insertions. +/datum/point_of_interest/proc/compare_to(datum/point_of_interest/rhs) + return cmp_name_asc(target, rhs.target) + +/datum/point_of_interest/mob_poi + poi_type = /mob + +/// Validation for mobs is expanded to invalidate stealthmins and /mob/dead/new_player as POIs. +/datum/point_of_interest/mob_poi/validate() + . = ..() + + if(!.) + return + + var/mob/poi_mob = target + + // Stealthmin, invalid as a POI. + if(poi_mob.client?.holder?.fakekey) + return FALSE + + // POI is a /mob/dead/new_player, players in the lobby are invalid as POIs. + if(isnewplayer(poi_mob)) + return FALSE + + return TRUE + +/// Mob POIs are sorted by a simple priority list depending on their type. When their type priority is identical, they're sub-sorted by name. +/datum/point_of_interest/mob_poi/compare_to(datum/point_of_interest/mob_poi/rhs) + var/sort_difference = get_type_sort_priority() - rhs.get_type_sort_priority() + + // If they're equal in priority, call parent to sort by name. + if(sort_difference == 0) + return ..() + // Else sort by priority. + else + return sort_difference + +/// Priority list broadly stolen from /proc/sortmobs(). Lower numbers are higher priorities when sorted and appear closer to the top or start of lists. +/datum/point_of_interest/mob_poi/proc/get_type_sort_priority() + if(isAI(target)) + return 0 + if(iscameramob(target)) + return 1 + if(ispAI(target)) + return 2 + if(iscyborg(target)) + return 3 + if(ishuman(target)) + return 4 + if(isbrain(target)) + return 5 + if(isalien(target)) + return 6 + if(isobserver(target)) + return 7 + if(isnewplayer(target)) + return 8 + if(isslime(target)) + return 9 + if(isanimal(target)) + return 10 + return 11 diff --git a/code/datums/elements/point_of_interest.dm b/code/datums/elements/point_of_interest.dm new file mode 100644 index 000000000000..3ab6c6296293 --- /dev/null +++ b/code/datums/elements/point_of_interest.dm @@ -0,0 +1,20 @@ +/// Designates the atom as a "point of interest", meaning it can be directly orbited +/datum/element/point_of_interest + element_flags = ELEMENT_DETACH + +/datum/element/point_of_interest/Attach(datum/target) + if (!isatom(target)) + return ELEMENT_INCOMPATIBLE + + // New players are abstract mobs assigned to people who are still in the lobby screen. + // As a result, they are not a valid POI and should never be a valid POI. If they + // somehow get this element attached to them, there's something we need to debug. + if(isnewplayer(target)) + return ELEMENT_INCOMPATIBLE + + SSpoints_of_interest.on_poi_element_added(target) + return ..() + +/datum/element/point_of_interest/Detach(datum/target) + SSpoints_of_interest.on_poi_element_removed(target) + return ..() diff --git a/code/datums/spawners_menu.dm b/code/datums/spawners_menu.dm index 9e7e4b334ddc..01709a00c1a1 100644 --- a/code/datums/spawners_menu.dm +++ b/code/datums/spawners_menu.dm @@ -53,7 +53,7 @@ if(!spawnerlist.len) return var/obj/effect/mob_spawn/MS = pick(spawnerlist) - if(!istype(MS) || !(MS in GLOB.poi_list)) + if(!istype(MS) || !(MS in SSpoints_of_interest.other_points_of_interest)) return switch(action) if("jump") diff --git a/code/game/atom/atom_orbit.dm b/code/game/atom/atom_orbit.dm new file mode 100644 index 000000000000..2294293bd8b7 --- /dev/null +++ b/code/game/atom/atom_orbit.dm @@ -0,0 +1,33 @@ +/atom + ///Reference to atom being orbited + var/atom/orbit_target + ///The orbiter component, if there's anything orbiting this atom + var/datum/component/orbiter/orbiters + +/** + * Recursive getter method to return a list of all ghosts orbitting this atom + * + * This will work fine without manually passing arguments. + * * processed - The list of atoms we've already convered + * * source - Is this the atom for who we're counting up all the orbiters? + * * ignored_stealthed_admins - If TRUE, don't count admins who are stealthmoded and orbiting this + */ +/atom/proc/get_all_orbiters(list/processed, source = TRUE, ignore_stealthed_admins = TRUE) + var/list/output = list() + if(!processed) + processed = list() + else if(src in processed) + return output + + if(!source) + output += src + + processed += src + for(var/atom/atom_orbiter as anything in orbiters?.orbiters) + output += atom_orbiter.get_all_orbiters(processed, source = FALSE) + return output + +/mob/get_all_orbiters(list/processed, source = TRUE, ignore_stealthed_admins = TRUE) + if(!source && ignore_stealthed_admins && client?.holder?.fakekey) + return list() + return ..() diff --git a/code/game/atoms.dm b/code/game/atoms.dm index f52b9bdace9e..b96e8a53c824 100644 --- a/code/game/atoms.dm +++ b/code/game/atoms.dm @@ -75,9 +75,6 @@ ///Economy cost of item in premium vendor var/custom_premium_price - //List of datums orbiting this atom - var/datum/component/orbiter/orbiters - /// Radiation insulation types var/rad_insulation = RAD_NO_INSULATION @@ -154,8 +151,6 @@ /// The current connector overlay appearance. Saved so that it can be cut when necessary. var/connector_overlay - ///Reference to atom being orbited - var/atom/orbit_target ///Default X pixel offset var/base_pixel_x ///Default Y pixel offset diff --git a/code/game/gamemodes/wizard/wizard.dm b/code/game/gamemodes/wizard/wizard.dm index c3895f302279..83d7a32d1fd9 100644 --- a/code/game/gamemodes/wizard/wizard.dm +++ b/code/game/gamemodes/wizard/wizard.dm @@ -56,7 +56,7 @@ if(isliving(wizard.current) && wizard.current.stat!=DEAD) return FALSE - for(var/obj/item/phylactery/P in GLOB.poi_list) //TODO : IsProperlyDead() + for(var/obj/item/phylactery/P in SSpoints_of_interest.other_points_of_interest) //TODO : IsProperlyDead() if(P.mind && P.mind.has_antag_datum(/datum/antagonist/wizard)) return FALSE diff --git a/code/game/mecha/mecha.dm b/code/game/mecha/mecha.dm index b903564c88d6..a1b46fd2fbfa 100644 --- a/code/game/mecha/mecha.dm +++ b/code/game/mecha/mecha.dm @@ -137,7 +137,7 @@ add_scanmod() add_capacitor() START_PROCESSING(SSobj, src) - GLOB.poi_list |= src + SSpoints_of_interest.make_point_of_interest(src) log_message("[src.name] created.", LOG_MECHA) GLOB.mechas_list += src //global mech list prepare_huds() @@ -176,7 +176,7 @@ AI.gib() //No wreck, no AI to recover AI = null STOP_PROCESSING(SSobj, src) - GLOB.poi_list.Remove(src) + SSpoints_of_interest.remove_point_of_interest(src) equipment.Cut() for(var/datum/atom_hud/data/diagnostic/diag_hud in GLOB.huds) diff --git a/code/game/objects/items/eightball.dm b/code/game/objects/items/eightball.dm index 1396521aaf9a..b721393e85ed 100644 --- a/code/game/objects/items/eightball.dm +++ b/code/game/objects/items/eightball.dm @@ -136,10 +136,10 @@ become_hearing_sensitive(ROUNDSTART_TRAIT) for (var/answer in haunted_answers) votes[answer] = 0 - GLOB.poi_list |= src + SSpoints_of_interest.make_point_of_interest(src) /obj/item/toy/eightball/haunted/Destroy() - GLOB.poi_list -= src + SSpoints_of_interest.remove_point_of_interest(src) . = ..() /obj/item/toy/eightball/haunted/MakeHaunted() diff --git a/code/modules/admin/player_panel.dm b/code/modules/admin/player_panel.dm index cf834c9f9c5d..e5888f8088df 100644 --- a/code/modules/admin/player_panel.dm +++ b/code/modules/admin/player_panel.dm @@ -216,9 +216,10 @@ "} - var/list/mobs = sortmobs() + var/list/mobs = SSpoints_of_interest.get_mob_pois() var/i = 1 - for(var/mob/M in mobs) + for(var/mob_name in mobs) + var/mob/M = mobs[mob_name] if(M.ckey) var/color = "#e6e6e6" @@ -254,12 +255,7 @@ M_job = "Silicon-based" else if(isanimal(M)) //simple animals - if(iscorgi(M)) - M_job = "Corgi" - else if(isslime(M)) - M_job = "slime" - else - M_job = "Animal" + M_job = "Animal" else M_job = "Living" diff --git a/code/modules/admin/verbs/adminjump.dm b/code/modules/admin/verbs/adminjump.dm index 9b84224317a9..708448cf52cd 100644 --- a/code/modules/admin/verbs/adminjump.dm +++ b/code/modules/admin/verbs/adminjump.dm @@ -140,7 +140,7 @@ usr.forceMove(M.loc) SSblackbox.record_feedback("tally", "admin_verb", 1, "Get Key") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc! -/client/proc/sendmob(mob/M in sortmobs()) +/client/proc/sendmob(mob/M in SSpoints_of_interest.get_mob_pois()) set category = "Admin.Game" set name = "Send Mob" if(!src.holder) diff --git a/code/modules/antagonists/cult/runes.dm b/code/modules/antagonists/cult/runes.dm index 5058dc73f081..d27ee061d462 100644 --- a/code/modules/antagonists/cult/runes.dm +++ b/code/modules/antagonists/cult/runes.dm @@ -458,10 +458,10 @@ structure_check() searches for nearby cultist structures required for the invoca /obj/effect/rune/narsie/Initialize(mapload, set_keyword) . = ..() - GLOB.poi_list |= src + SSpoints_of_interest.make_point_of_interest(src) /obj/effect/rune/narsie/Destroy() - GLOB.poi_list -= src + SSpoints_of_interest.remove_point_of_interest(src) . = ..() /obj/effect/rune/narsie/conceal() //can't hide this, and you wouldn't want to @@ -477,7 +477,7 @@ structure_check() searches for nearby cultist structures required for the invoca if(!(place in summon_objective.summon_spots)) to_chat(user, "The Geometer can only be summoned where the veil is weak - in [english_list(summon_objective.summon_spots)]!") return - if(locate(/obj/singularity/narsie) in GLOB.poi_list) + if(locate(/obj/singularity/narsie) in SSpoints_of_interest.other_points_of_interest) for(var/M in invokers) to_chat(M, "Nar'Sie is already on this plane!") log_game("Nar'Sie rune failed - already summoned") diff --git a/code/modules/antagonists/nukeop/equipment/nuclearbomb.dm b/code/modules/antagonists/nukeop/equipment/nuclearbomb.dm index 920fbbdd557b..d53d1bc4cca0 100644 --- a/code/modules/antagonists/nukeop/equipment/nuclearbomb.dm +++ b/code/modules/antagonists/nukeop/equipment/nuclearbomb.dm @@ -38,7 +38,7 @@ core = new /obj/item/nuke_core(src) STOP_PROCESSING(SSobj, core) update_appearance() - GLOB.poi_list |= src + SSpoints_of_interest.make_point_of_interest(src) previous_level = get_security_level() /obj/machinery/nuclearbomb/Destroy() @@ -46,7 +46,7 @@ if(!exploding) // If we're not exploding, set the alert level back to normal set_safety() - GLOB.poi_list -= src + SSpoints_of_interest.remove_point_of_interest(src) GLOB.nuke_list -= src QDEL_NULL(countdown) QDEL_NULL(core) @@ -611,7 +611,7 @@ This is here to make the tiles around the station mininuke change when it's arme AddElement(/datum/element/bed_tuckable, 6, -6, 0) if(!fake) - GLOB.poi_list |= src + SSpoints_of_interest.make_point_of_interest(src) last_disk_move = world.time START_PROCESSING(SSobj, src) @@ -675,7 +675,7 @@ This is here to make the tiles around the station mininuke change when it's arme /obj/item/disk/nuclear/Destroy(force=FALSE) // respawning is handled in /obj/Destroy() if(force) - GLOB.poi_list -= src + SSpoints_of_interest.remove_point_of_interest(src) . = ..() /obj/item/disk/nuclear/fake diff --git a/code/modules/antagonists/nukeop/equipment/pinpointer.dm b/code/modules/antagonists/nukeop/equipment/pinpointer.dm index b316e60c5e14..c1f9ffa37428 100644 --- a/code/modules/antagonists/nukeop/equipment/pinpointer.dm +++ b/code/modules/antagonists/nukeop/equipment/pinpointer.dm @@ -32,7 +32,7 @@ target = null switch(mode) if(TRACK_NUKE_DISK) - var/obj/item/disk/nuclear/N = locate() in GLOB.poi_list + var/obj/item/disk/nuclear/N = locate() in SSpoints_of_interest.other_points_of_interest target = N if(TRACK_MALF_AI) for(var/V in GLOB.ai_list) diff --git a/code/modules/antagonists/nukeop/nukeop.dm b/code/modules/antagonists/nukeop/nukeop.dm index 9f807d9521e7..f51c64142450 100644 --- a/code/modules/antagonists/nukeop/nukeop.dm +++ b/code/modules/antagonists/nukeop/nukeop.dm @@ -345,7 +345,7 @@ /datum/team/nuclear/antag_listing_entry() var/disk_report = "Nuclear Disk(s)
" disk_report += "
" - for(var/obj/item/disk/nuclear/N in GLOB.poi_list) + for(var/obj/item/disk/nuclear/N in SSpoints_of_interest.other_points_of_interest) disk_report += "
[N.name], " var/atom/disk_loc = N.loc while(!isturf(disk_loc)) diff --git a/code/modules/awaymissions/capture_the_flag.dm b/code/modules/awaymissions/capture_the_flag.dm index 2f87eea6d2a7..17458c661f1b 100644 --- a/code/modules/awaymissions/capture_the_flag.dm +++ b/code/modules/awaymissions/capture_the_flag.dm @@ -194,10 +194,10 @@ /obj/machinery/capture_the_flag/Initialize() . = ..() - GLOB.poi_list |= src + SSpoints_of_interest.make_point_of_interest(src) /obj/machinery/capture_the_flag/Destroy() - GLOB.poi_list.Remove(src) + SSpoints_of_interest.remove_point_of_interest(src) return ..() /obj/machinery/capture_the_flag/process() diff --git a/code/modules/awaymissions/corpse.dm b/code/modules/awaymissions/corpse.dm index 0bf0b74c715a..26361b99cfc6 100644 --- a/code/modules/awaymissions/corpse.dm +++ b/code/modules/awaymissions/corpse.dm @@ -61,11 +61,11 @@ if(instant || (roundstart && (mapload || (SSticker && SSticker.current_state > GAME_STATE_SETTING_UP)))) INVOKE_ASYNC(src, PROC_REF(create)) else if(ghost_usable) - GLOB.poi_list |= src + SSpoints_of_interest.make_point_of_interest(src) LAZYADD(GLOB.mob_spawners[name], src) /obj/effect/mob_spawn/Destroy() - GLOB.poi_list -= src + SSpoints_of_interest.remove_point_of_interest(src) var/list/spawners = GLOB.mob_spawners[name] LAZYREMOVE(spawners, src) if(!LAZYLEN(spawners)) diff --git a/code/modules/cargo/centcom_podlauncher.dm b/code/modules/cargo/centcom_podlauncher.dm index dfec659e1644..0b302925e10e 100644 --- a/code/modules/cargo/centcom_podlauncher.dm +++ b/code/modules/cargo/centcom_podlauncher.dm @@ -383,7 +383,7 @@ if (specificTarget) specificTarget = null return - var/list/mobs = getpois()//code stolen from observer.dm + var/list/mobs = SSpoints_of_interest.get_mob_pois() var/inputTarget = input("Select a mob! (Smiting does this automatically)", "Target", null, null) as null|anything in mobs if (isnull(inputTarget)) return diff --git a/code/modules/events/immovable_rod.dm b/code/modules/events/immovable_rod.dm index adde1124935f..4ba2d878ad46 100644 --- a/code/modules/events/immovable_rod.dm +++ b/code/modules/events/immovable_rod.dm @@ -62,7 +62,7 @@ In my current plan for it, 'solid' will be defined as anything with density == 1 z_original = z destination = end special_target = aimed_at - GLOB.poi_list += src + SSpoints_of_interest.make_point_of_interest(src) var/special_target_valid = FALSE if(special_target) @@ -81,7 +81,7 @@ In my current plan for it, 'solid' will be defined as anything with density == 1 ghost.ManualFollow(src) /obj/effect/immovablerod/Destroy() - GLOB.poi_list -= src + SSpoints_of_interest.remove_point_of_interest(src) . = ..() /obj/effect/immovablerod/Moved() diff --git a/code/modules/events/wizard/greentext.dm b/code/modules/events/wizard/greentext.dm index 890bbc0f1f2b..8e4baab893fc 100644 --- a/code/modules/events/wizard/greentext.dm +++ b/code/modules/events/wizard/greentext.dm @@ -34,7 +34,7 @@ /obj/item/greentext/Initialize(mapload) . = ..() - GLOB.poi_list |= src + SSpoints_of_interest.make_point_of_interest(src) roundend_callback = CALLBACK(src, PROC_REF(check_winner)) SSticker.OnRoundend(roundend_callback) @@ -83,7 +83,7 @@ if(!(resistance_flags & ON_FIRE) && !force) return QDEL_HINT_LETMELIVE - GLOB.poi_list.Remove(src) + SSpoints_of_interest.remove_point_of_interest(src) LAZYREMOVE(SSticker.round_end_events, roundend_callback) roundend_callback = null //This ought to free the callback datum, and prevent us from harddeling for(var/i in GLOB.player_list) diff --git a/code/modules/mining/lavaland/necropolis_chests.dm b/code/modules/mining/lavaland/necropolis_chests.dm index a2b48d9319b9..4bfa9c67e674 100644 --- a/code/modules/mining/lavaland/necropolis_chests.dm +++ b/code/modules/mining/lavaland/necropolis_chests.dm @@ -1234,7 +1234,7 @@ . = ..() spirits = list() START_PROCESSING(SSobj, src) - GLOB.poi_list |= src + SSpoints_of_interest.make_point_of_interest(src) AddComponent(/datum/component/butchering, 150, 90) /obj/item/melee/ghost_sword/Destroy() @@ -1242,7 +1242,7 @@ G.invisibility = GLOB.observer_default_invisibility spirits.Cut() STOP_PROCESSING(SSobj, src) - GLOB.poi_list -= src + SSpoints_of_interest.remove_point_of_interest(src) . = ..() /obj/item/melee/ghost_sword/attack_self(mob/user) diff --git a/code/modules/mob/dead/observer/observer.dm b/code/modules/mob/dead/observer/observer.dm index c15c4a1af835..ceff585ea460 100644 --- a/code/modules/mob/dead/observer/observer.dm +++ b/code/modules/mob/dead/observer/observer.dm @@ -62,6 +62,9 @@ GLOBAL_VAR_INIT(observer_default_invisibility, INVISIBILITY_OBSERVER) var/datum/orbit_menu/orbit_menu var/datum/spawners_menu/spawners_menu + // The POI we're orbiting (orbit menu) + var/orbiting_ref + /mob/dead/observer/Initialize() set_invisibility(GLOB.observer_default_invisibility) @@ -142,6 +145,8 @@ GLOBAL_VAR_INIT(observer_default_invisibility, INVISIBILITY_OBSERVER) . = ..() + SSpoints_of_interest.make_point_of_interest(src) + grant_all_languages() show_data_huds() data_huds_on = 1 @@ -499,7 +504,7 @@ This is the proc mobs get to turn into a ghost. Forked from ghostize due to comp var/list/dest = list() //List of possible destinations (mobs) var/target = null //Chosen target. - dest += getpois(mobs_only = TRUE) //Fill list, prompt user with list + dest += SSpoints_of_interest.get_mob_pois() target = input("Please, select a player!", "Jump to Mob", null, null) as null|anything in dest if (!target)//Make sure we actually have a target @@ -852,7 +857,7 @@ This is the proc mobs get to turn into a ghost. Forked from ghostize due to comp set name = "Observe" set category = "Ghost" - var/list/creatures = getpois() + var/list/creatures = SSpoints_of_interest.get_mob_pois() reset_perspective(null) diff --git a/code/modules/mob/dead/observer/orbit.dm b/code/modules/mob/dead/observer/orbit.dm index 86d54577538c..eb1c54df7934 100644 --- a/code/modules/mob/dead/observer/orbit.dm +++ b/code/modules/mob/dead/observer/orbit.dm @@ -1,6 +1,9 @@ /datum/orbit_menu + ///mobs worth orbiting. Because spaghetti, all mobs have the point of interest, but only some are allowed to actually show up. + ///this obviously should be changed in the future, so we only add mobs as POI if they actually are interesting, and we don't use + ///a typecache. + var/static/list/mob_allowed_typecache var/mob/dead/observer/owner - var/auto_observe = FALSE /datum/orbit_menu/New(mob/dead/observer/new_owner) if(!istype(new_owner)) @@ -23,87 +26,240 @@ switch(action) if ("orbit") var/ref = params["ref"] - var/atom/movable/poi = (locate(ref) in GLOB.mob_list) || (locate(ref) in GLOB.poi_list) - if (poi == null) - . = TRUE - return + var/auto_observe = params["auto_observe"] + var/atom/movable/poi = SSpoints_of_interest.get_poi_atom_by_ref(ref) + + if((ismob(poi) && !SSpoints_of_interest.is_valid_poi(poi, CALLBACK(src, PROC_REF(validate_mob_poi)))) \ + || !SSpoints_of_interest.is_valid_poi(poi) + ) + to_chat(usr, span_notice("That point of interest is no longer valid.")) + return TRUE + + var/mob/dead/observer/user = usr owner.ManualFollow(poi) owner.reset_perspective(null) + user.orbiting_ref = ref if (auto_observe) owner.do_observe(poi) . = TRUE if ("refresh") update_static_data(owner, ui) . = TRUE - if ("toggle_observe") - auto_observe = !auto_observe - if (auto_observe && owner.orbit_target) - owner.do_observe(owner.orbit_target) - else - owner.reset_perspective(null) + /datum/orbit_menu/ui_data(mob/user) var/list/data = list() - data["auto_observe"] = auto_observe + + if(isobserver(user)) + data["orbiting"] = get_currently_orbiting(user) + return data /datum/orbit_menu/ui_static_data(mob/user) - var/list/data = list() + var/list/new_mob_pois = SSpoints_of_interest.get_mob_pois(CALLBACK(src, PROC_REF(validate_mob_poi)), append_dead_role = FALSE) + var/list/new_other_pois = SSpoints_of_interest.get_other_pois() var/list/alive = list() var/list/antagonists = list() + var/list/critical = list() var/list/dead = list() var/list/ghosts = list() var/list/misc = list() var/list/npcs = list() + var/list/ships = list() - var/list/pois = getpois(skip_mindless = TRUE, specify_dead_role = FALSE) - for (var/name in pois) + for(var/name in new_mob_pois) var/list/serialized = list() - serialized["name"] = name - - var/poi = pois[name] - - serialized["ref"] = REF(poi) - - var/mob/M = poi - if (istype(M)) - if (isobserver(M)) - ghosts += list(serialized) - else if (M.stat == DEAD) - dead += list(serialized) - else if (M.mind == null) - npcs += list(serialized) - else - var/number_of_orbiters = M.orbiters?.orbiters?.len - if (number_of_orbiters) - serialized["orbiters"] = number_of_orbiters - - var/datum/mind/mind = M.mind - var/was_antagonist = FALSE - - for (var/_A in mind.antag_datums) - var/datum/antagonist/A = _A - if (A.show_to_ghosts) - was_antagonist = TRUE - serialized["antag"] = A.name - antagonists += list(serialized) - break - - if (!was_antagonist) - alive += list(serialized) + var/mob/mob_poi = new_mob_pois[name] + var/number_of_orbiters = length(mob_poi.get_all_orbiters()) + + serialized["ref"] = REF(mob_poi) + serialized["full_name"] = mob_poi.name + serialized["job"] = mob_poi.job + if(number_of_orbiters) + serialized["orbiters"] = number_of_orbiters + + if(isobserver(mob_poi)) + ghosts += list(serialized) + continue + + if(mob_poi.stat == DEAD) + dead += list(serialized) + continue + + if(isnull(mob_poi.mind)) + if(isliving(mob_poi)) + var/mob/living/npc = mob_poi + serialized["health"] = FLOOR((npc.health / npc.maxHealth * 100), 1) + + npcs += list(serialized) + continue + + serialized["client"] = !!mob_poi.client + serialized["name"] = mob_poi.real_name + + if(isliving(mob_poi)) + serialized += get_living_data(mob_poi) + + var/list/antag_data = get_antag_data(mob_poi.mind) + if(length(antag_data)) + serialized += antag_data + antagonists += list(serialized) + continue + + alive += list(serialized) + + for(var/name in new_other_pois) + var/atom/atom_poi = new_other_pois[name] + + var/list/other_data = get_misc_data(atom_poi) + var/misc_data = list(other_data[1]) + + if(istype(atom_poi, /obj/machinery/computer/helm)) + ships += misc_data else - misc += list(serialized) - - data["alive"] = alive - data["antagonists"] = antagonists - data["dead"] = dead - data["ghosts"] = ghosts - data["misc"] = misc - data["npcs"] = npcs - return data + misc += misc_data + + if(other_data[2]) // Critical = TRUE + critical += misc_data + + return list( + "alive" = alive, + "antagonists" = antagonists, + "critical" = critical, + "dead" = dead, + "ghosts" = ghosts, + "misc" = misc, + "npcs" = npcs, + "ships" = ships, + ) /datum/orbit_menu/ui_assets() . = ..() || list() . += get_asset_datum(/datum/asset/simple/orbit) +/// Helper function to get threat type, group, overrides for job and icon +/datum/orbit_menu/proc/get_antag_data(datum/mind/poi_mind) as /list + var/list/serialized = list() + + for(var/datum/antagonist/antag as anything in poi_mind.antag_datums) + if(!antag.show_to_ghosts) + continue + + serialized["antag"] = antag.name + serialized["antag_group"] = antag.antagpanel_category + serialized["job"] = antag.name + serialized["icon"] = antag.antag_hud_name + + return serialized + +/// Helper to get the current thing we're orbiting (if any) +/datum/orbit_menu/proc/get_currently_orbiting(mob/dead/observer/user) + if(isnull(user.orbiting_ref)) + return + + var/atom/poi = SSpoints_of_interest.get_poi_atom_by_ref(user.orbiting_ref) + if(isnull(poi)) + user.orbiting_ref = null + return + + if((ismob(poi) && !SSpoints_of_interest.is_valid_poi(poi, CALLBACK(src, PROC_REF(validate_mob_poi)))) \ + || !SSpoints_of_interest.is_valid_poi(poi) + ) + user.orbiting_ref = null + return + + var/list/serialized = list() + + if(!ismob(poi)) + var/list/misc_info = get_misc_data(poi) + serialized += misc_info[1] + return serialized + + var/mob/mob_poi = poi + serialized["full_name"] = mob_poi.name + serialized["ref"] = REF(poi) + + if(mob_poi.mind) + serialized["client"] = !!mob_poi.client + serialized["name"] = mob_poi.real_name + + if(isliving(mob_poi)) + serialized += get_living_data(mob_poi) + + return serialized + +/// Helper function to get job / icon / health data for a living mob +/datum/orbit_menu/proc/get_living_data(mob/living/player) as /list + var/list/serialized = list() + + serialized["health"] = FLOOR((player.health / player.maxHealth * 100), 1) + + return serialized + + +/// Gets a list: Misc data and whether it's critical. Handles all snowflakey type cases +/datum/orbit_menu/proc/get_misc_data(atom/movable/atom_poi) as /list + var/list/misc = list() + var/critical = FALSE + + misc["ref"] = REF(atom_poi) + misc["full_name"] = atom_poi.name + + // Display the nuke timer + if(istype(atom_poi, /obj/machinery/nuclearbomb)) + var/obj/machinery/nuclearbomb/bomb = atom_poi + + if(bomb.timing) + misc["extra"] = "Timer: [bomb.countdown?.displayed_text]s" + critical = TRUE + + return list(misc, critical) + + // Display the holder if its a nuke disk + if(istype(atom_poi, /obj/item/disk/nuclear)) + var/obj/item/disk/nuclear/disk = atom_poi + var/mob/holder = disk.pulledby || get(disk, /mob) + misc["extra"] = "Location: [holder?.real_name || "Unsecured"]" + + return list(misc, critical) + + // Display singuloths if they exist + if(istype(atom_poi, /obj/singularity)) + var/obj/singularity/singulo = atom_poi + misc["extra"] = "Energy: [round(singulo.energy)]" + + if(singulo.current_size > 2) + critical = TRUE + + return list(misc, critical) + + if(istype(atom_poi, /obj/machinery/computer/helm)) + var/obj/machinery/computer/helm/helm_poi = atom_poi + if(helm_poi.current_ship) + misc["extra"] = "Ship: [helm_poi.current_ship.name]" + + return list(misc, critical) + + return list(misc, critical) + +/** + * Helper POI validation function passed as a callback to various SSpoints_of_interest procs. + * + * Provides extended validation above and beyond standard, limiting mob POIs without minds or ckeys + * unless they're mobs, camera mobs or megafauna. Also allows exceptions for mobs that are deadchat controlled. + * + * If they satisfy that requirement, falls back to default validation for the POI. + */ +/datum/orbit_menu/proc/validate_mob_poi(datum/point_of_interest/mob_poi/potential_poi) + var/mob/potential_mob_poi = potential_poi.target + if(!potential_mob_poi.mind && !potential_mob_poi.ckey) + if(!mob_allowed_typecache) + mob_allowed_typecache = typecacheof(list( + /mob/living/simple_animal/hostile/megafauna, + /mob/living/simple_animal/hostile/boss + )) + if(!is_type_in_typecache(potential_mob_poi, mob_allowed_typecache) && !potential_mob_poi.GetComponent(/datum/component/deadchat_control)) + return FALSE + + return potential_poi.validate() diff --git a/code/modules/mob/living/living.dm b/code/modules/mob/living/living.dm index 1258df8b84c5..6046cfe82b63 100644 --- a/code/modules/mob/living/living.dm +++ b/code/modules/mob/living/living.dm @@ -9,6 +9,7 @@ diag_hud.add_to_hud(src) faction += "[REF(src)]" GLOB.mob_living_list += src + SSpoints_of_interest.make_point_of_interest(src) if(speed) update_living_varspeed() diff --git a/code/modules/mob/living/simple_animal/friendly/drone/drones_as_items.dm b/code/modules/mob/living/simple_animal/friendly/drone/drones_as_items.dm index e0537594c8ff..76dc1f095009 100644 --- a/code/modules/mob/living/simple_animal/friendly/drone/drones_as_items.dm +++ b/code/modules/mob/living/simple_animal/friendly/drone/drones_as_items.dm @@ -30,11 +30,6 @@ var/area/A = get_area(src) if(A) notify_ghosts("A drone shell has been created in \the [A.name].", source = src, action=NOTIFY_ATTACK, flashwindow = FALSE, ignore_key = POLL_IGNORE_DRONE) - GLOB.poi_list |= src - -/obj/effect/mob_spawn/drone/Destroy() - GLOB.poi_list -= src - . = ..() //ATTACK GHOST IGNORING PARENT RETURN VALUE /obj/effect/mob_spawn/drone/attack_ghost(mob/user) diff --git a/code/modules/mob/living/simple_animal/hostile/megafauna/colossus.dm b/code/modules/mob/living/simple_animal/hostile/megafauna/colossus.dm index 863abf56dad1..ff06df9c6113 100644 --- a/code/modules/mob/living/simple_animal/hostile/megafauna/colossus.dm +++ b/code/modules/mob/living/simple_animal/hostile/megafauna/colossus.dm @@ -607,12 +607,12 @@ GLOBAL_DATUM(blackbox, /obj/machinery/smartfridge/black_box) var/ready_to_deploy = FALSE /obj/machinery/anomalous_crystal/helpers/Destroy() - GLOB.poi_list -= src + SSpoints_of_interest.remove_point_of_interest(src) . = ..() /obj/machinery/anomalous_crystal/helpers/ActivationReaction(mob/user, method) if(..() && !ready_to_deploy) - GLOB.poi_list |= src + SSpoints_of_interest.make_point_of_interest(src) ready_to_deploy = TRUE notify_ghosts("An anomalous crystal has been activated in [get_area(src)]! This crystal can always be used by ghosts hereafter.", enter_link = "(Click to enter)", ghost_sound = 'sound/effects/ghost2.ogg', source = src, action = NOTIFY_ATTACK, header = "Anomalous crystal activated") diff --git a/code/modules/modular_computers/file_system/programs/radar.dm b/code/modules/modular_computers/file_system/programs/radar.dm index d082503a012d..06324b8f8b78 100644 --- a/code/modules/modular_computers/file_system/programs/radar.dm +++ b/code/modules/modular_computers/file_system/programs/radar.dm @@ -156,7 +156,7 @@ *something like "mob_209". In order to find the actual atom, we need *to search the appropriate list for the REF string. This is dependant *on the program (Lifeline uses GLOB.human_list, while Fission360 uses - *GLOB.poi_list), but the result will be the same; evaluate the string and + *SSpoints_of_interest.other_points_of_interest), but the result will be the same; evaluate the string and *return an atom reference. */ /datum/computer_file/program/radar/proc/find_atom() @@ -269,7 +269,7 @@ pointercolor = "red" /datum/computer_file/program/radar/fission360/find_atom() - return locate(selected) in GLOB.poi_list + return locate(selected) in SSpoints_of_interest.other_points_of_interest /datum/computer_file/program/radar/fission360/scan() if(world.time < next_scan) @@ -286,7 +286,7 @@ name = nuke.name, ) objects += list(nukeinfo) - var/obj/item/disk/nuclear/disk = locate() in GLOB.poi_list + var/obj/item/disk/nuclear/disk = locate() in SSpoints_of_interest.other_points_of_interest if(trackable(disk)) var/list/nukeinfo = list( ref = REF(disk), diff --git a/code/modules/overmap/helm.dm b/code/modules/overmap/helm.dm index 5b1f27fa2cb9..3b825ce39524 100644 --- a/code/modules/overmap/helm.dm +++ b/code/modules/overmap/helm.dm @@ -49,6 +49,8 @@ /obj/machinery/computer/helm/Initialize(mapload, obj/item/circuitboard/C) . = ..() + if(!viewer) + SSpoints_of_interest.make_point_of_interest(src) jump_allowed = world.time + CONFIG_GET(number/bluespace_jump_wait) ntnet_relay = new(src) @@ -76,6 +78,7 @@ SStgui.close_uis(src) ASSERT(length(concurrent_users) == 0) QDEL_NULL(ntnet_relay) + SSpoints_of_interest.remove_point_of_interest(src) if(current_ship) current_ship.helms -= src current_ship = null diff --git a/code/modules/power/singularity/singularity.dm b/code/modules/power/singularity/singularity.dm index 9218b829e940..499a88b161f1 100644 --- a/code/modules/power/singularity/singularity.dm +++ b/code/modules/power/singularity/singularity.dm @@ -38,7 +38,7 @@ src.energy = starting_energy . = ..() START_PROCESSING(SSobj, src) - GLOB.poi_list |= src + SSpoints_of_interest.make_point_of_interest(src) GLOB.singularities |= src for(var/obj/machinery/power/singularity_beacon/singubeacon in GLOB.machines) if(singubeacon.active) @@ -53,7 +53,7 @@ /obj/singularity/Destroy() STOP_PROCESSING(SSobj, src) - GLOB.poi_list.Remove(src) + SSpoints_of_interest.remove_point_of_interest(src) GLOB.singularities.Remove(src) return ..() diff --git a/code/modules/power/supermatter/supermatter.dm b/code/modules/power/supermatter/supermatter.dm index b6fc6367e85c..3d94de599aab 100644 --- a/code/modules/power/supermatter/supermatter.dm +++ b/code/modules/power/supermatter/supermatter.dm @@ -275,7 +275,7 @@ GLOBAL_DATUM(main_supermatter_engine, /obj/machinery/power/supermatter_crystal) SSair.start_processing_machine(src, mapload) countdown = new(src) countdown.start() - GLOB.poi_list |= src + SSpoints_of_interest.make_point_of_interest(src) radio = new(src) radio.keyslot = new radio_key radio.listening = 0 @@ -293,7 +293,7 @@ GLOBAL_DATUM(main_supermatter_engine, /obj/machinery/power/supermatter_crystal) investigate_log("has been destroyed.", INVESTIGATE_SUPERMATTER) SSair.stop_processing_machine(src) QDEL_NULL(radio) - GLOB.poi_list -= src + SSpoints_of_interest.remove_point_of_interest(src) QDEL_NULL(countdown) if(is_main_engine && GLOB.main_supermatter_engine == src) GLOB.main_supermatter_engine = null diff --git a/code/modules/power/tesla/energy_ball.dm b/code/modules/power/tesla/energy_ball.dm index 7d58610fc7f7..d4a6f71cca4d 100644 --- a/code/modules/power/tesla/energy_ball.dm +++ b/code/modules/power/tesla/energy_ball.dm @@ -151,7 +151,7 @@ /obj/singularity/energy_ball/orbit(obj/singularity/energy_ball/target) if (istype(target)) target.orbiting_balls += src - GLOB.poi_list -= src + SSpoints_of_interest.remove_point_of_interest(src) target.dissipate_strength = target.orbiting_balls.len . = ..() diff --git a/code/modules/projectiles/guns/energy/pulse.dm b/code/modules/projectiles/guns/energy/pulse.dm index c2e5b4cb2933..40fd10e94785 100644 --- a/code/modules/projectiles/guns/energy/pulse.dm +++ b/code/modules/projectiles/guns/energy/pulse.dm @@ -46,7 +46,7 @@ /obj/item/gun/energy/pulse/prize/Initialize() . = ..() - GLOB.poi_list += src + SSpoints_of_interest.make_point_of_interest(src) var/turf/T = get_turf(src) message_admins("A pulse rifle prize has been created at [ADMIN_VERBOSEJMP(T)]") @@ -55,7 +55,7 @@ notify_ghosts("Someone won a pulse rifle as a prize!", source = src, action = NOTIFY_ORBIT, header = "Pulse rifle prize") /obj/item/gun/energy/pulse/prize/Destroy() - GLOB.poi_list -= src + SSpoints_of_interest.remove_point_of_interest(src) . = ..() /obj/item/gun/energy/pulse/pistol diff --git a/code/modules/spells/spell_types/lichdom.dm b/code/modules/spells/spell_types/lichdom.dm index 720670e3fc21..38e2f0df145e 100644 --- a/code/modules/spells/spell_types/lichdom.dm +++ b/code/modules/spells/spell_types/lichdom.dm @@ -99,7 +99,7 @@ name = "phylactery of [mind.name]" active_phylacteries++ - GLOB.poi_list |= src + SSpoints_of_interest.make_point_of_interest(src) START_PROCESSING(SSobj, src) if(initial(SSticker.mode.round_ends_with_antag_death)) SSticker.mode.round_ends_with_antag_death = FALSE @@ -107,7 +107,7 @@ /obj/item/phylactery/Destroy(force=FALSE) STOP_PROCESSING(SSobj, src) active_phylacteries-- - GLOB.poi_list -= src + SSpoints_of_interest.remove_point_of_interest(src) if(!active_phylacteries) SSticker.mode.round_ends_with_antag_death = initial(SSticker.mode.round_ends_with_antag_death) . = ..() diff --git a/shiptest.dme b/shiptest.dme index 55ad11a800d5..0e379318cbf9 100644 --- a/shiptest.dme +++ b/shiptest.dme @@ -366,6 +366,7 @@ #include "code\controllers\subsystem\persistence.dm" #include "code\controllers\subsystem\physics.dm" #include "code\controllers\subsystem\ping.dm" +#include "code\controllers\subsystem\points_of_interest.dm" #include "code\controllers\subsystem\profiler.dm" #include "code\controllers\subsystem\radiation.dm" #include "code\controllers\subsystem\radio.dm" @@ -669,6 +670,7 @@ #include "code\datums\elements\lazy_fishing_spot.dm" #include "code\datums\elements\light_blocking.dm" #include "code\datums\elements\mobappearance.dm" +#include "code\datums\elements\point_of_interest.dm" #include "code\datums\elements\renamemob.dm" #include "code\datums\elements\selfknockback.dm" #include "code\datums\elements\snail_crawl.dm" @@ -854,6 +856,7 @@ #include "code\game\area\areas\ruins\space.dm" #include "code\game\area\areas\ruins\templates.dm" #include "code\game\area\areas\ruins\wasteplanet.dm" +#include "code\game\atom\atom_orbit.dm" #include "code\game\gamemodes\events.dm" #include "code\game\gamemodes\game_mode.dm" #include "code\game\gamemodes\objective.dm" diff --git a/tgui/packages/tgui/interfaces/Orbit.js b/tgui/packages/tgui/interfaces/Orbit.js deleted file mode 100644 index 91bf9d1f7929..000000000000 --- a/tgui/packages/tgui/interfaces/Orbit.js +++ /dev/null @@ -1,218 +0,0 @@ -import { createSearch } from 'common/string'; -import { multiline } from 'common/string'; -import { resolveAsset } from '../assets'; -import { useBackend, useLocalState } from '../backend'; -import { - Box, - Button, - Divider, - Flex, - Icon, - Input, - Section, -} from '../components'; -import { Window } from '../layouts'; - -const PATTERN_NUMBER = / \(([0-9]+)\)$/; - -const searchFor = (searchText) => - createSearch(searchText, (thing) => thing.name); - -const compareString = (a, b) => (a < b ? -1 : a > b); - -const compareNumberedText = (a, b) => { - const aName = a.name; - const bName = b.name; - - // Check if aName and bName are the same except for a number at the end - // e.g. Medibot (2) and Medibot (3) - const aNumberMatch = aName.match(PATTERN_NUMBER); - const bNumberMatch = bName.match(PATTERN_NUMBER); - - if ( - aNumberMatch && - bNumberMatch && - aName.replace(PATTERN_NUMBER, '') === bName.replace(PATTERN_NUMBER, '') - ) { - const aNumber = parseInt(aNumberMatch[1], 10); - const bNumber = parseInt(bNumberMatch[1], 10); - - return aNumber - bNumber; - } - - return compareString(aName, bName); -}; - -const BasicSection = (props, context) => { - const { act } = useBackend(context); - const { searchText, source, title } = props; - const things = source.filter(searchFor(searchText)); - things.sort(compareNumberedText); - return ( - source.length > 0 && ( -
- {things.map((thing) => ( -
- ) - ); -}; - -const OrbitedButton = (props, context) => { - const { act } = useBackend(context); - const { color, thing } = props; - - return ( - - ); -}; - -export const Orbit = (props, context) => { - const { act, data } = useBackend(context); - const { alive, antagonists, auto_observe, dead, ghosts, misc, npcs } = data; - - const [searchText, setSearchText] = useLocalState(context, 'searchText', ''); - - const collatedAntagonists = {}; - for (const antagonist of antagonists) { - if (collatedAntagonists[antagonist.antag] === undefined) { - collatedAntagonists[antagonist.antag] = []; - } - collatedAntagonists[antagonist.antag].push(antagonist); - } - - const sortedAntagonists = Object.entries(collatedAntagonists); - sortedAntagonists.sort((a, b) => { - return compareString(a[0], b[0]); - }); - - const orbitMostRelevant = (searchText) => { - for (const source of [ - sortedAntagonists.map(([_, antags]) => antags), - alive, - ghosts, - dead, - npcs, - misc, - ]) { - const member = source - .filter(searchFor(searchText)) - .sort(compareNumberedText)[0]; - if (member !== undefined) { - act('orbit', { ref: member.ref }); - break; - } - } - }; - - return ( - - -
- - - - - - setSearchText(value)} - onEnter={(_, value) => orbitMostRelevant(value)} - /> - - - - - -
- {antagonists.length > 0 && ( -
- {sortedAntagonists.map(([name, antags]) => ( -
- {antags - .filter(searchFor(searchText)) - .sort(compareNumberedText) - .map((antag) => ( - - ))} -
- ))} -
- )} - -
- {alive - .filter(searchFor(searchText)) - .sort(compareNumberedText) - .map((thing) => ( - - ))} -
- -
- {ghosts - .filter(searchFor(searchText)) - .sort(compareNumberedText) - .map((thing) => ( - - ))} -
- - - - - - -
-
- ); -}; diff --git a/tgui/packages/tgui/interfaces/Orbit/OrbitContent.tsx b/tgui/packages/tgui/interfaces/Orbit/OrbitContent.tsx new file mode 100644 index 000000000000..f3c59a75e189 --- /dev/null +++ b/tgui/packages/tgui/interfaces/Orbit/OrbitContent.tsx @@ -0,0 +1,98 @@ +import { toTitleCase } from 'common/string'; + +import { useBackend } from '../../backend'; +import { NoticeBox, Section, Stack, Table, Tooltip } from '../../components'; + +import { getAntagCategories } from './helpers'; +import { AntagGroup, Observable, OrbitData } from './types'; +import { OrbitSection } from './OrbitSection'; + +type ContentSection = { + content: Observable[]; + title: string; + color?: string; +}; + +export const OrbitContent = (props, context) => { + const { act, data } = useBackend(context); + const { antagonists = [], critical = [] } = data; + const { searchText, autoObserve } = props; + + let antagGroups: AntagGroup[] = []; + if (antagonists.length) { + antagGroups = getAntagCategories(antagonists); + } + + const sections: readonly ContentSection[] = [ + { + content: data.alive, + title: 'Alive', + color: 'good', + }, + { + content: data.dead, + title: 'Dead', + }, + { + content: data.ghosts, + title: 'Ghosts', + }, + { + content: data.misc, + title: 'Misc', + }, + { + content: data.npcs, + title: 'NPCs', + }, + { + content: data.ships, + title: 'Ships', + }, + ]; + + return ( +
+ + {critical.map((crit) => ( + + act('orbit', { ref: crit.ref })} + > + + + {toTitleCase(crit.full_name)} + {crit.extra} + +
+
+
+ ))} + + {antagGroups.map(([title, members]) => ( + + ))} + + {sections.map((section) => ( + + ))} +
+
+ ); +}; diff --git a/tgui/packages/tgui/interfaces/Orbit/OrbitItem.tsx b/tgui/packages/tgui/interfaces/Orbit/OrbitItem.tsx new file mode 100644 index 000000000000..957104afa8b9 --- /dev/null +++ b/tgui/packages/tgui/interfaces/Orbit/OrbitItem.tsx @@ -0,0 +1,50 @@ +import { useBackend } from '../../backend'; +import { Stack, Button, Flex, Icon } from '../../components'; + +import { capitalizeFirst } from 'common/string'; + +import { getDisplayColor, getDisplayName } from './helpers'; +import { Antagonist, Observable, OrbitData } from './types'; + +type Props = { + item: Observable | Antagonist; + autoObserve: boolean; + color: string | undefined; +}; + +export const OrbitItem = (props: Props, context) => { + const { item, autoObserve, color } = props; + const { full_name, icon, job, name, orbiters, ref } = item; + + const { act, data } = useBackend(context); + const { orbiting } = data; + + const selected = ref === orbiting?.ref; + const validIcon = !!job && !!icon && icon !== 'hudunknown'; + + return ( + act('orbit', { auto_observe: autoObserve, ref })} + style={{ + display: 'flex', + }} + > + + + ); +}; diff --git a/tgui/packages/tgui/interfaces/Orbit/OrbitSection.tsx b/tgui/packages/tgui/interfaces/Orbit/OrbitSection.tsx new file mode 100644 index 000000000000..b81ba27c1f27 --- /dev/null +++ b/tgui/packages/tgui/interfaces/Orbit/OrbitSection.tsx @@ -0,0 +1,65 @@ +import { Collapsible, Flex, Tooltip } from '../../components'; +import { isJobOrNameMatch } from './helpers'; +import { OrbitItem } from './OrbitItem'; +import { OrbitTooltip } from './OrbitTooltip'; +import { Observable } from './types'; + +type Props = { + color?: string; + section: Observable[]; + title: string; + searchQuery: string; + autoObserve: boolean; +}; + +/** + * Displays a collapsible with a map of observable items. + * Filters the results if there is a provided search query. + */ +export const OrbitSection = (props: Props) => { + const { color, section = [], title, searchQuery, autoObserve } = props; + + const filteredSection = section.filter((observable) => + isJobOrNameMatch(observable, searchQuery) + ); + + if (!filteredSection.length) { + return <>; + } + + return ( + + + {filteredSection.map((item) => { + const content = ( + + ); + + if (!item.health && !item.extra) { + return content; + } + + return ( + } + key={item.ref} + position="bottom-start" + > + {content} + + ); + })} + + + ); +}; diff --git a/tgui/packages/tgui/interfaces/Orbit/OrbitTooltip.tsx b/tgui/packages/tgui/interfaces/Orbit/OrbitTooltip.tsx new file mode 100644 index 000000000000..0a941dcb0fb5 --- /dev/null +++ b/tgui/packages/tgui/interfaces/Orbit/OrbitTooltip.tsx @@ -0,0 +1,57 @@ +import { LabeledList, NoticeBox } from '../../components'; +import { Antagonist, Observable } from './types'; + +type Props = { + item: Observable | Antagonist; +}; + +/** Displays some info on the mob as a tooltip. */ +export const OrbitTooltip = (props: Props) => { + const { item } = props; + const { extra, name, full_name, health, job } = item; + + let antag; + if ('antag' in item) { + antag = item.antag; + } + + const extraInfo = extra?.split(':'); + const displayHealth = !!health && health >= 0 ? `${health}%` : 'Critical'; + const showAFK = 'client' in item && !item.client; + + return ( + <> + + Last Known Data + + + {extraInfo ? ( + + {extraInfo[1]} + + ) : ( + <> + {!!name && ( + {name} + )} + {!!full_name && ( + + {full_name} + + )} + {!!job && {job}} + {!!antag && ( + {antag} + )} + {!!health && ( + + {displayHealth} + + )} + + )} + {showAFK && Away} + + + ); +}; diff --git a/tgui/packages/tgui/interfaces/Orbit/constants.ts b/tgui/packages/tgui/interfaces/Orbit/constants.ts new file mode 100644 index 000000000000..1edadc9bfe45 --- /dev/null +++ b/tgui/packages/tgui/interfaces/Orbit/constants.ts @@ -0,0 +1,8 @@ +export const HEALTH = { + Good: 69, // nice + Average: 19, + Bad: 0, + Crit: -30, + Dead: -100, + Ruined: -200, +} as const; diff --git a/tgui/packages/tgui/interfaces/Orbit/helpers.ts b/tgui/packages/tgui/interfaces/Orbit/helpers.ts new file mode 100644 index 000000000000..e502446e855c --- /dev/null +++ b/tgui/packages/tgui/interfaces/Orbit/helpers.ts @@ -0,0 +1,113 @@ +import { createSearch } from '../../../common/string'; + +import { HEALTH } from './constants'; +import { AntagGroup, Antagonist, Observable } from './types'; + +const PATTERN_NUMBER = / \(([0-9]+)\)$/; + +/** Return a map of strings with each antag in its antag_category */ +export const getAntagCategories = (antagonists: Antagonist[]): AntagGroup[] => { + const categories = new Map(); + + for (const player of antagonists) { + const { antag_group } = player; + + if (!categories.has(antag_group)) { + categories.set(antag_group, []); + } + categories.get(antag_group)!.push(player); + } + + const sorted = Array.from(categories.entries()).sort((a, b) => { + const lowerA = a[0].toLowerCase(); + const lowerB = b[0].toLowerCase(); + + if (lowerA < lowerB) return -1; + if (lowerA > lowerB) return 1; + return 0; + }); + + return sorted; +}; + +/** Returns a disguised name in case the person is wearing someone else's ID */ +export const getDisplayName = (full_name: string, nickname?: string): string => { + if (!nickname) { + return full_name; + } + + return nickname; +}; + +/** Displays color for buttons based on the health or orbiter count. */ +export const getDisplayColor = (item: Observable, override?: string): string => { + const { job, health, orbiters } = item; + + // Things like blob camera, etc + if (typeof health !== 'number') { + return override ? 'good' : 'grey'; + } + + // Players that are AFK + if ('client' in item && !item.client) { + return 'grey'; + } + + return getHealthColor(health); +}; + +/** Returns the display color for certain health percentages */ +const getHealthColor = (health: number): string => { + switch (true) { + case health > HEALTH.Good: + return 'good'; + case health > HEALTH.Average: + return 'average'; + default: + return 'bad'; + } +}; + +/** Checks if a full name or job title matches the search. */ +export const isJobOrNameMatch = ( + observable: Observable, + searchQuery: string +): boolean => { + if (!searchQuery) return true; + + const { full_name, job } = observable; + + return ( + full_name?.toLowerCase().includes(searchQuery?.toLowerCase()) || + job?.toLowerCase().includes(searchQuery?.toLowerCase()) || + false + ); +}; + +export const searchFor = (searchText) => + createSearch(searchText, (thing) => thing.name); + +export const compareString = (a, b) => (a < b ? -1 : a > b); + +export const compareNumberedText = (a, b) => { + const aName = a.name; + const bName = b.name; + + // Check if aName and bName are the same except for a number at the end + // e.g. Medibot (2) and Medibot (3) + const aNumberMatch = aName.match(PATTERN_NUMBER); + const bNumberMatch = bName.match(PATTERN_NUMBER); + + if ( + aNumberMatch && + bNumberMatch && + aName.replace(PATTERN_NUMBER, '') === bName.replace(PATTERN_NUMBER, '') + ) { + const aNumber = parseInt(aNumberMatch[1], 10); + const bNumber = parseInt(bNumberMatch[1], 10); + + return aNumber - bNumber; + } + + return compareString(aName, bName); +}; diff --git a/tgui/packages/tgui/interfaces/Orbit/index.tsx b/tgui/packages/tgui/interfaces/Orbit/index.tsx new file mode 100644 index 000000000000..05ce4e2539f5 --- /dev/null +++ b/tgui/packages/tgui/interfaces/Orbit/index.tsx @@ -0,0 +1,86 @@ +import { multiline } from '../../../common/string'; +import { useBackend, useLocalState } from '../../backend'; +import { Button, Divider, Flex, Icon, Input, Section } from '../../components'; +import { Window } from '../../layouts'; + +import { searchFor } from './helpers'; +import { OrbitData } from './types'; +import { OrbitContent } from './OrbitContent'; + +export const Orbit = (props, context) => { + const { act, data } = useBackend(context); + + const [searchText, setSearchText] = useLocalState(context, 'searchText', ''); + const [autoObserve, setAutoObserve] = useLocalState( + context, + 'autoObserve', + false + ); + + const orbitMostRelevant = () => { + const mostRelevant = [ + data.antagonists, + data.alive, + data.ghosts, + data.dead, + data.npcs, + data.misc, + data.ships, + ] + .flat() + .filter(searchFor(searchText)) + .sort()[0]; + + if (mostRelevant !== undefined) { + act('orbit', { ref: mostRelevant.ref }); + } + }; + + return ( + + +
+ + + + + + setSearchText(value)} + onEnter={(_, value) => orbitMostRelevant()} + /> + + + + + +
+ +
+
+ ); +}; diff --git a/tgui/packages/tgui/interfaces/Orbit/types.ts b/tgui/packages/tgui/interfaces/Orbit/types.ts new file mode 100644 index 000000000000..4912ae10db4c --- /dev/null +++ b/tgui/packages/tgui/interfaces/Orbit/types.ts @@ -0,0 +1,38 @@ +import { BooleanLike } from '../../../common/react'; + +export type Antagonist = Observable & { antag: string; antag_group: string }; + +export type AntagGroup = [string, Antagonist[]]; + +export type OrbitData = { + alive: Observable[]; + antagonists: Antagonist[]; + critical: Critical[]; + dead: Observable[]; + ghosts: Observable[]; + misc: Observable[]; + npcs: Observable[]; + ships: Observable[]; + orbiting: Observable | null; + autoObserve: boolean; +}; + +export type Observable = { + full_name: string; + ref: string; + // Optionals +} & Partial<{ + client: BooleanLike; + extra: string; + health: number; + icon: string; + job: string; + name: string; + orbiters: number; +}>; + +type Critical = { + extra: string; + full_name: string; + ref: string; +};