Files
Bubberstation/code/game/objects/items/pinpointer.dm
SyncIt21 0f57a23830 Refactor for storage initialization & organization (#89543)
## About The Pull Request
A Huge chunk of changes just comes from moving existing storage code
into new files & seperating `atom_storage` code into its own subtype
under the already existing `storage/subtypes` folder.

With that the changes in this PR can be organized into 3 categories.

**1. Refactors how `/obj/item/storage/PopulateContents()` initializes
storages**
- Fixes #88747 and every other storage item that has a similar variant
of this problem

The problem with `PopulateContents()` is that it allows you to create
atoms directly inside the storage via `new(src)` thus bypassing all the
access restrictions enforced by `/datum/storage/can_insert()` resulting
in storages holding stuff they shouldn't be able to hold.

Now how this proc works has been changed. It must now only return a list
of items(each item in the list can either be a typepath or a solid atom
or a mix of them in any order) that should be inserted into the storage.
Each item is then passed into `can_insert()` to check if it can fit in
the storage.

If your list contains solid atoms they must be first moved
to/Initialized in nullspace so `can_insert()` won't count it as already
inserted. `can_insert()` has now also been refactored to throw stack
traces but explaining exactly why the item could not fit in the storage
thus giving you more debugging details to fix your stuff.

A large majority of changes is refactoring `PopulateContents()` to
return a list instead of simply creating the item in place so simple 1
line changes & with that we have fixed all broken storages(medical
toolbox. electrical toolbox, cruisader armor boxes & many more) that
hold more items they can handle

**2. Organizes initialization of `atom_storage` for storage subtypes.**
All subtypes of `/obj/item/storage` should(not enforced) create their
own `/datum/storage/` subtype under the folder `storage/subtypes` if the
default values are not sufficient. This is the 2nd change done across
all existing storages

Not only does this bring code cleanliness & organization (separating
storage code from item code like how `/datum/wire` code is separated
into its own sub folder) but it also makes storage initialization
slightly faster (because you are not modifying default values after
`atom_storage` is initialized but you are directly setting the default
value in place).

You now cannot & should not modify `atom_storage` values inside
`PopulateContents()`. This will make that proc as pure as possible so
less side effects. Of course this principle is not enforced and you can
still modify the storage value after `Initialize()` but this should not
be encouraged in the future

**3. Adds support for automatic storage computations**
Most people don't understand how `atom_storage` values work. The comment
here clearly states that

55bbfef0da/code/game/objects/items/storage/toolbox.dm (L327-L329)
Because of that the linked issue occurs not just for medical toolbox but
for a lot of other items as well.

Which is why if you do not know what you doing, `PopulateContents()` now
comes with a new storage parameter i.e. `/datum/storage_config`

This datum allows you to compute storage values that will perfectly fit
with the initial contents of your storage. It allows you to do stuff
like computing `max_slots`, `max_item_weight`, `max_total_weight` etc
based on your storage initial contents so that all the contents can fit
perfectly leaving no space for excess.

## Changelog
🆑
fix: storages are no longer initialized with items that can't be put
back in after taking them out
refactor: storage initialization has been refactored. Please report bugs
on github
/🆑
2025-03-23 22:20:23 +01:00

256 lines
8.4 KiB
Plaintext

//Pinpointers are used to track atoms from a distance as long as they're on the same z-level. The captain and nuke ops have ones that track the nuclear authentication disk.
/obj/item/pinpointer
name = "pinpointer"
desc = "A handheld tracking device that locks onto certain signals."
icon = 'icons/obj/devices/tracker.dmi'
icon_state = "pinpointer"
obj_flags = CONDUCTS_ELECTRICITY
slot_flags = ITEM_SLOT_BELT
w_class = WEIGHT_CLASS_SMALL
inhand_icon_state = "electronic"
worn_icon_state = "pinpointer"
lefthand_file = 'icons/mob/inhands/items/devices_lefthand.dmi'
righthand_file = 'icons/mob/inhands/items/devices_righthand.dmi'
throw_speed = 3
throw_range = 7
custom_materials = list(/datum/material/iron = SMALL_MATERIAL_AMOUNT * 5, /datum/material/glass = SMALL_MATERIAL_AMOUNT * 2.5)
var/active = FALSE
var/atom/movable/target //The thing we're searching for
var/minimum_range = 0 //at what range the pinpointer declares you to be at your destination
var/alert = FALSE // TRUE to display things more seriously
var/process_scan = TRUE // some pinpointers change target every time they scan, which means we can't have it change very process but instead when it turns on.
var/icon_suffix = "" // for special pinpointer icons
/obj/item/pinpointer/Initialize(mapload)
. = ..()
GLOB.pinpointer_list += src
/obj/item/pinpointer/Destroy()
STOP_PROCESSING(SSfastprocess, src)
GLOB.pinpointer_list -= src
target = null
return ..()
/obj/item/pinpointer/attack_self(mob/living/user)
if(!process_scan) //since it's not scanning on process, it scans here.
scan_for_target()
toggle_on()
user.visible_message(span_notice("[user] [active ? "" : "de"]activates [user.p_their()] pinpointer."), span_notice("You [active ? "" : "de"]activate your pinpointer."))
/obj/item/pinpointer/examine(mob/user)
. = ..()
if(target)
. += "It is currently tracking [target]."
/obj/item/pinpointer/proc/toggle_on()
active = !active
playsound(src, 'sound/items/tools/screwdriver2.ogg', 50, TRUE)
if(active)
START_PROCESSING(SSfastprocess, src)
else
target = null
STOP_PROCESSING(SSfastprocess, src)
update_appearance()
/obj/item/pinpointer/process()
if(!active)
return PROCESS_KILL
if(process_scan)
scan_for_target()
update_appearance()
/obj/item/pinpointer/proc/scan_for_target()
return
/obj/item/pinpointer/update_overlays()
. = ..()
if(!active)
return
if(!target)
. += "pinon[alert ? "alert" : ""]null[icon_suffix]"
return
var/turf/here = get_turf(src)
var/turf/there = get_turf(target)
if(!here || !there || here.z != there.z)
. += "pinon[alert ? "alert" : ""]null[icon_suffix]"
return
. += get_direction_icon(here, there)
///Called by update_icon after sanity. There is a target
/obj/item/pinpointer/proc/get_direction_icon(here, there)
if(get_dist_euclidean(here,there) <= minimum_range)
return "pinon[alert ? "alert" : ""]direct[icon_suffix]"
else
setDir(get_dir(here, there))
switch(get_dist(here, there))
if(1 to 8)
return "pinon[alert ? "alert" : "close"][icon_suffix]"
if(9 to 16)
return "pinon[alert ? "alert" : "medium"][icon_suffix]"
if(16 to INFINITY)
return "pinon[alert ? "alert" : "far"][icon_suffix]"
/obj/item/pinpointer/crew // A replacement for the old crew monitoring consoles
name = "crew pinpointer"
desc = "A handheld tracking device that points to crew suit sensors."
icon_state = "pinpointer_crew"
worn_icon_state = "pinpointer_crew"
custom_price = PAYCHECK_CREW * 6
custom_premium_price = PAYCHECK_CREW * 6
var/has_owner = FALSE
var/pinpointer_owner = null
var/ignore_suit_sensor_level = FALSE /// Do we find people even if their suit sensors are turned off
/obj/item/pinpointer/crew/proc/trackable(mob/living/carbon/human/H)
var/turf/here = get_turf(src)
var/turf/there = get_turf(H)
if(here && there && (there.z == here.z || (is_station_level(here.z) && is_station_level(there.z)))) // Device and target should be on the same level or different levels of the same station
if (istype(H.w_uniform, /obj/item/clothing/under))
var/obj/item/clothing/under/U = H.w_uniform
if(U.has_sensor && (U.sensor_mode >= SENSOR_COORDS || ignore_suit_sensor_level)) // Suit sensors must be on maximum or a contractor pinpointer
return TRUE
return FALSE
/obj/item/pinpointer/crew/attack_self(mob/living/user)
if(active)
toggle_on()
user.visible_message(span_notice("[user] deactivates [user.p_their()] pinpointer."), span_notice("You deactivate your pinpointer."))
return
if (has_owner && !pinpointer_owner)
pinpointer_owner = user
if (pinpointer_owner && pinpointer_owner != user)
to_chat(user, span_notice("The pinpointer doesn't respond. It seems to only recognise its owner."))
return
var/list/name_counts = list()
var/list/names = list()
for(var/i in GLOB.human_list)
var/mob/living/carbon/human/H = i
if(!trackable(H))
continue
var/crewmember_name = "Unknown"
if(H.wear_id)
var/obj/item/card/id/I = H.wear_id.GetID()
if(I?.registered_name)
crewmember_name = I.registered_name
while(crewmember_name in name_counts)
name_counts[crewmember_name]++
crewmember_name = "[crewmember_name] ([name_counts[crewmember_name]])"
names[crewmember_name] = H
name_counts[crewmember_name] = 1
if(!length(names))
user.visible_message(span_notice("[user]'s pinpointer fails to detect a signal."), span_notice("Your pinpointer fails to detect a signal."))
return
var/pinpoint_target = tgui_input_list(user, "Person to track", "Pinpoint", sort_list(names))
if(isnull(pinpoint_target))
return
if(isnull(names[pinpoint_target]))
return
if(QDELETED(src) || !user || !user.is_holding(src) || user.incapacitated)
return
target = names[pinpoint_target]
toggle_on()
user.visible_message(span_notice("[user] activates [user.p_their()] pinpointer."), span_notice("You activate your pinpointer."))
/obj/item/pinpointer/crew/scan_for_target()
if(target)
if(ishuman(target))
var/mob/living/carbon/human/H = target
if(!trackable(H))
target = null
if(!target) //target can be set to null from above code, or elsewhere
active = FALSE
/obj/item/pinpointer/pair
name = "pair pinpointer"
desc = "A handheld tracking device that locks onto its other half of the matching pair."
var/other_pair
/obj/item/pinpointer/pair/Destroy()
other_pair = null
. = ..()
/obj/item/pinpointer/pair/scan_for_target()
target = other_pair
/obj/item/pinpointer/pair/examine(mob/user)
. = ..()
if(!active || !target)
return
var/mob/mob_holder = get(target, /mob)
if(istype(mob_holder))
. += "Its pair is being held by [mob_holder]."
return
/obj/item/storage/box/pinpointer_pairs
name = "pinpointer pair box"
/obj/item/storage/box/pinpointer_pairs/PopulateContents()
var/obj/item/pinpointer/pair/A = new(null)
var/obj/item/pinpointer/pair/B = new(null)
A.other_pair = B
B.other_pair = A
return list(A, B)
/obj/item/pinpointer/shuttle
name = "bounty shuttle pinpointer"
desc = "A handheld tracking device that locates the bounty hunter shuttle for quick escapes."
icon_state = "pinpointer_hunter"
worn_icon_state = "pinpointer_black"
icon_suffix = "_hunter"
var/obj/shuttleport
/obj/item/pinpointer/shuttle/Initialize(mapload)
. = ..()
shuttleport = SSshuttle.getShuttle("huntership")
/obj/item/pinpointer/shuttle/scan_for_target()
target = shuttleport
/obj/item/pinpointer/shuttle/Destroy()
shuttleport = null
. = ..()
///list of all sheets with sniffable = TRUE for the sniffer to locate
GLOBAL_LIST_EMPTY(sniffable_sheets)
/obj/item/pinpointer/material_sniffer
name = "material sniffer"
desc = "A handheld tracking device that locates sheets of glass and iron."
icon_state = "pinpointer_sniffer"
worn_icon_state = "pinpointer_black"
/obj/item/pinpointer/material_sniffer/scan_for_target()
if(target || !GLOB.sniffable_sheets.len)
return
var/obj/item/stack/sheet/new_sheet_target
var/closest_distance = INFINITY
for(var/obj/item/stack/sheet/potential_sheet as anything in GLOB.sniffable_sheets)
// not enough for lag reasons, and shouldn't even be on this
if(potential_sheet.amount < 10)
GLOB.sniffable_sheets -= potential_sheet
continue
//held by someone
if(isliving(potential_sheet.loc))
continue
//not on scanner's z
if(potential_sheet.z != z)
continue
var/distance_from_sniffer = get_dist(src, potential_sheet)
if(distance_from_sniffer < closest_distance)
closest_distance = distance_from_sniffer
new_sheet_target = potential_sheet
if(!new_sheet_target)
target = null
return
say("Located [new_sheet_target.amount] [new_sheet_target.singular_name]s!")
target = new_sheet_target