diff --git a/code/__DEFINES/traits/monkestation/declarations.dm b/code/__DEFINES/traits/monkestation/declarations.dm
index 8a07f00fb954..a26127eb0ff5 100644
--- a/code/__DEFINES/traits/monkestation/declarations.dm
+++ b/code/__DEFINES/traits/monkestation/declarations.dm
@@ -121,6 +121,8 @@
#define TRAIT_BYPASS_COMPRESS_CHECK "can_compress_anyways"
/// This item is considered "trash" (and will be eaten by cleaner slimes)
#define TRAIT_TRASH_ITEM "trash_item"
+/// This item came from a gift.
+#define TRAIT_GIFT_ITEM "gift_item"
// /atom/movable
/// Things with this trait can pass through wooden barricades.
diff --git a/code/__DEFINES/~monkestation/vv.dm b/code/__DEFINES/~monkestation/vv.dm
new file mode 100644
index 000000000000..8acf0c7ef495
--- /dev/null
+++ b/code/__DEFINES/~monkestation/vv.dm
@@ -0,0 +1 @@
+#define VV_HK_EXAMINE_GIFT "examine_gift"
diff --git a/code/__HELPERS/~monkestation-helpers/atoms.dm b/code/__HELPERS/~monkestation-helpers/atoms.dm
index 67b81a50dafb..44f26ac804e0 100644
--- a/code/__HELPERS/~monkestation-helpers/atoms.dm
+++ b/code/__HELPERS/~monkestation-helpers/atoms.dm
@@ -26,3 +26,11 @@
default_typecache ||= typecacheof(list(/obj/effect, /atom/movable/screen))
typecache = default_typecache
return typecache_filter_list_reverse(src.contents, typecache)
+
+/// Returns a list of all items in our contents that were obtained from gifts.
+/atom/proc/get_all_gift_contents() as /list
+ RETURN_TYPE(/list/obj/item)
+ . = list()
+ for(var/obj/item/thing as anything in get_all_contents_type(/obj/item))
+ if(!QDELETED(thing) && HAS_TRAIT(thing, TRAIT_GIFT_ITEM))
+ . += thing
diff --git a/code/_globalvars/traits/_traits.dm b/code/_globalvars/traits/_traits.dm
index 7734282dd4d7..12590729049c 100644
--- a/code/_globalvars/traits/_traits.dm
+++ b/code/_globalvars/traits/_traits.dm
@@ -611,24 +611,31 @@ GLOBAL_LIST_INIT(traits_by_type, list(
),
/obj/item = list(
"TRAIT_APC_SHOCKING" = TRAIT_APC_SHOCKING,
+ "TRAIT_ASSISTED_BREATHING" = TRAIT_ASSISTED_BREATHING,
"TRAIT_BASIC_QUALITY_BAIT" = TRAIT_BASIC_QUALITY_BAIT,
"TRAIT_BELT_SATCHEL" = TRAIT_BELT_SATCHEL,
"TRAIT_BLIND_TOOL" = TRAIT_BLIND_TOOL,
"TRAIT_BYPASS_COMPRESS_CHECK" = TRAIT_BYPASS_COMPRESS_CHECK,
"TRAIT_CUSTOM_TAP_SOUND" = TRAIT_CUSTOM_TAP_SOUND,
"TRAIT_DANGEROUS_OBJECT" = TRAIT_DANGEROUS_OBJECT,
+ "TRAIT_FEATHERED" = TRAIT_FEATHERED,
"TRAIT_FISHING_BAIT" = TRAIT_FISHING_BAIT,
"TRAIT_FOOD_GRILLED" = TRAIT_FOOD_GRILLED,
+ "TRAIT_GIFT_ITEM" = TRAIT_GIFT_ITEM,
"TRAIT_GOOD_QUALITY_BAIT" = TRAIT_GOOD_QUALITY_BAIT,
"TRAIT_GREAT_QUALITY_BAIT" = TRAIT_GREAT_QUALITY_BAIT,
"TRAIT_HAUNTED" = TRAIT_HAUNTED,
"TRAIT_HONKSPAMMING" = TRAIT_HONKSPAMMING,
"TRAIT_INNATELY_FANTASTICAL_ITEM" = TRAIT_INNATELY_FANTASTICAL_ITEM,
+ "TRAIT_INSTANTLY_PROCESSES_BOULDERS" = TRAIT_INSTANTLY_PROCESSES_BOULDERS,
+ "TRAIT_LABOURED_BREATHING" = TRAIT_LABOURED_BREATHING,
"TRAIT_MAT_TRANSMUTED" = TRAIT_MAT_TRANSMUTED,
"TRAIT_MAY_CONTAIN_BLENDED_DUST" = TRAIT_MAY_CONTAIN_BLENDED_DUST,
"TRAIT_NEEDS_TWO_HANDS" = TRAIT_NEEDS_TWO_HANDS,
"TRAIT_NODROP" = TRAIT_NODROP,
+ "TRAIT_NON_IMPORTANT_SHOE_BLOCK" = TRAIT_NON_IMPORTANT_SHOE_BLOCK,
"TRAIT_NO_BARCODES" = TRAIT_NO_BARCODES,
+ "TRAIT_NO_ORGAN_DECAY" = TRAIT_NO_ORGAN_DECAY,
"TRAIT_NO_STORAGE_INSERT" = TRAIT_NO_STORAGE_INSERT,
"TRAIT_NO_TELEPORT" = TRAIT_NO_TELEPORT,
"TRAIT_OMNI_BAIT" = TRAIT_OMNI_BAIT,
@@ -639,17 +646,11 @@ GLOBAL_LIST_INIT(traits_by_type, list(
"TRAIT_T_RAY_VISIBLE" = TRAIT_T_RAY_VISIBLE,
"TRAIT_UNCATCHABLE" = TRAIT_UNCATCHABLE,
"TRAIT_WIELDED" = TRAIT_WIELDED,
- "TRAIT_FEATHERED" = TRAIT_FEATHERED,
- "TRAIT_NON_IMPORTANT_SHOE_BLOCK" = TRAIT_NON_IMPORTANT_SHOE_BLOCK,
- "TRAIT_LABOURED_BREATHING" = TRAIT_LABOURED_BREATHING,
- "TRAIT_ASSISTED_BREATHING" = TRAIT_ASSISTED_BREATHING,
- "TRAIT_NO_ORGAN_DECAY" = TRAIT_NO_ORGAN_DECAY,
/* "TRAIT_BAIT_UNCONSUMABLE" = TRAIT_BAIT_UNCONSUMABLE, */
/* "TRAIT_BAKEABLE" = TRAIT_BAKEABLE, */
/* "TRAIT_BYPASS_RANGED_ARMOR" = TRAIT_BYPASS_RANGED_ARMOR, */
/* "TRAIT_CONTRABAND_BLOCKER" = TRAIT_CONTRABAND_BLOCKER, */
/* "TRAIT_GERM_SENSITIVE" = TRAIT_GERM_SENSITIVE, */
- "TRAIT_INSTANTLY_PROCESSES_BOULDERS" = TRAIT_INSTANTLY_PROCESSES_BOULDERS,
/* "TRAIT_ITEM_OBJECTIVE_BLOCKED" = TRAIT_ITEM_OBJECTIVE_BLOCKED, */
/* "TRAIT_NO_SIDE_KICK" = TRAIT_NO_SIDE_KICK, */
),
diff --git a/code/game/objects/items/gift.dm b/code/game/objects/items/gift.dm
index c1a5e10a1b38..16913a80c04d 100644
--- a/code/game/objects/items/gift.dm
+++ b/code/game/objects/items/gift.dm
@@ -50,6 +50,7 @@ GLOBAL_LIST_EMPTY(possible_gifts)
M.investigate_log("has unwrapped a present containing [I.type].", INVESTIGATE_PRESENTS)
M.put_in_hands(I)
I.add_fingerprint(M)
+ I.AddComponent(/datum/component/gift_item, M) // monkestation edit: gift item info component
else
M.visible_message(span_danger("Oh no! The present that [M] opened had nothing inside it!"))
diff --git a/code/modules/admin/verbs/admingame.dm b/code/modules/admin/verbs/admingame.dm
index 670b2c76ec76..340cc3d17faa 100644
--- a/code/modules/admin/verbs/admingame.dm
+++ b/code/modules/admin/verbs/admingame.dm
@@ -45,6 +45,18 @@
full_version = "[M.client.byond_version].[M.client.byond_build ? M.client.byond_build : "xxx"]"
body += "
\[Byond version: [full_version]\]
"
+ // monkestation start: gift info
+ var/list/gifts = M.get_all_gift_contents()
+ var/gift_amt = length(gifts)
+ if(gift_amt)
+ body += "
Gift items in contents:
"
+ for(var/idx in 1 to gift_amt)
+ var/obj/item/gift = gifts[idx]
+ body += VV_HREF_TARGET(gift, VV_HK_EXAMINE_GIFT, "[gift.name] ([gift.type])")
+ if(idx < gift_amt)
+ body += "
"
+ // monkestation end
+
body += "
\[ "
body += "VV - "
diff --git a/monkestation/code/datums/components/gift_item.dm b/monkestation/code/datums/components/gift_item.dm
new file mode 100644
index 000000000000..d6f82e9fa89a
--- /dev/null
+++ b/monkestation/code/datums/components/gift_item.dm
@@ -0,0 +1,86 @@
+/// Simple thing that marks an items as having come from a gift.
+/datum/component/gift_item
+ /// The ckey of the player who opened the gift.
+ var/ckey
+ /// Weakref to the mob who opened the gift.
+ var/datum/weakref/giftee
+ /// Weakref to the mob who opened the gift.
+ var/datum/weakref/mind
+ /// The (real) name of mob who opened the gift.
+ var/name
+ /// The `world.time` when the gift was opened.
+ var/open_world_time
+ /// The `world.timeofday` when the gift was opened.
+ var/open_timeofday
+
+/datum/component/gift_item/Initialize(mob/living/giftee)
+ if(!isitem(parent))
+ stack_trace("Tried to assign [type] to a non-item")
+ return COMPONENT_INCOMPATIBLE
+ if(!isliving(giftee))
+ stack_trace("Tried to assign [type] to something that wasn't a living mob!")
+ return COMPONENT_INCOMPATIBLE
+ if(!giftee.ckey)
+ stack_trace("Tried to assign [type] to a non-player mob!")
+ return COMPONENT_INCOMPATIBLE
+ src.ckey = giftee.ckey
+ src.giftee = WEAKREF(giftee)
+ src.mind = WEAKREF(giftee.mind)
+ src.name = "[giftee.mind?.name || giftee.real_name || giftee.name || "N/A"]"
+ src.open_world_time = world.time
+ src.open_timeofday = world.timeofday
+
+/datum/component/gift_item/Destroy(force)
+ giftee = null
+ mind = null
+ return ..()
+
+/datum/component/gift_item/RegisterWithParent()
+ RegisterSignal(parent, COMSIG_ATOM_EXAMINE, PROC_REF(on_examine))
+ RegisterSignal(parent, COMSIG_ATOM_EXAMINE_MORE, PROC_REF(on_examine_more))
+ RegisterSignal(parent, COMSIG_VV_TOPIC, PROC_REF(handle_vv_topic))
+ ADD_TRAIT(parent, TRAIT_GIFT_ITEM, type)
+
+/datum/component/gift_item/UnregisterFromParent()
+ UnregisterSignal(parent, list(COMSIG_ATOM_EXAMINE, COMSIG_ATOM_EXAMINE_MORE, COMSIG_VV_TOPIC))
+ REMOVE_TRAIT(parent, TRAIT_GIFT_ITEM, type)
+
+/datum/component/gift_item/proc/on_examine(obj/item/source, mob/examiner, list/examine_text)
+ SIGNAL_HANDLER
+ if(check_rights_for(examiner.client, R_ADMIN))
+ // ensure we always target the right mob for the admin buttons
+ var/mob/target_mob = resolve_opener_mob()
+ examine_text += ""
+ examine_text += span_bold("\[") + span_info(" This item came from a gift opened by [span_name(name)] ([ckey]) [ADMIN_FULLMONTY_NONAME(target_mob)] ") + span_bold("\]")
+ examine_text += span_bold("\[") + span_info(" It was unwrapped from a gift [span_bold(DisplayTimeText(world.time - open_world_time) + " ago")], at server time [span_bold(time2text(open_timeofday, "YYYY-MM-DD hh:mm:ss"))] ") + span_bold("\]")
+ examine_text += ""
+ else if(isobserver(examiner) || HAS_TRAIT(examiner, TRAIT_PRESENT_VISION) || SSticker.current_state >= GAME_STATE_FINISHED)
+ examine_text += ""
+ examine_text += span_bold("\[") + span_info(" This item came from a gift opened by [span_name(name)] [DisplayTimeText(world.time - open_world_time)] ago ") + span_bold("\]")
+ examine_text += ""
+
+/datum/component/gift_item/proc/on_examine_more(obj/item/source, mob/examiner, list/examine_text)
+ SIGNAL_HANDLER
+ examine_text += span_info("This item seems to have been a gift!")
+
+/datum/component/gift_item/proc/resolve_opener_mob() as /mob
+ RETURN_TYPE(/mob)
+ var/mob/opener = giftee.resolve()
+ var/datum/mind/opener_mind = mind.resolve()
+ if(opener?.ckey == ckey)
+ return opener
+ else if(opener_mind?.current?.ckey == ckey)
+ return opener_mind.current
+ else if(GLOB.directory[ckey])
+ var/client/current_client = GLOB.directory[ckey]
+ return current_client.mob
+ else
+ for(var/mob/mob in GLOB.mob_list)
+ if(mob.ckey == ckey)
+ return mob
+
+/datum/component/gift_item/proc/handle_vv_topic(datum/source, mob/user, list/href_list)
+ SIGNAL_HANDLER
+ if(href_list[VV_HK_EXAMINE_GIFT] && check_rights(R_ADMIN))
+ user.examinate(parent)
+ return COMPONENT_VV_HANDLED
diff --git a/tgstation.dme b/tgstation.dme
index 12c9484b8077..830016c00216 100644
--- a/tgstation.dme
+++ b/tgstation.dme
@@ -469,6 +469,7 @@
#include "code\__DEFINES\~monkestation\twitch.dm"
#include "code\__DEFINES\~monkestation\uplink.dm"
#include "code\__DEFINES\~monkestation\virology.dm"
+#include "code\__DEFINES\~monkestation\vv.dm"
#include "code\__DEFINES\~monkestation\dcs\signals\signals_atom.dm"
#include "code\__DEFINES\~monkestation\dcs\signals\signals_blueshift.dm"
#include "code\__DEFINES\~monkestation\dcs\signals\signals_carbon.dm"
@@ -5955,6 +5956,7 @@
#include "monkestation\code\datums\components\carbon_sprint.dm"
#include "monkestation\code\datums\components\charge_adjuster.dm"
#include "monkestation\code\datums\components\crafting.dm"
+#include "monkestation\code\datums\components\gift_item.dm"
#include "monkestation\code\datums\components\gps.dm"
#include "monkestation\code\datums\components\irradiated.dm"
#include "monkestation\code\datums\components\lock_on_cursor.dm"