diff --git a/_maps/map_files/RandomRuins/SpaceRuins/meatpackers.dmm b/_maps/map_files/RandomRuins/SpaceRuins/meatpackers.dmm
index 6a22394fcae..067579f3be8 100644
--- a/_maps/map_files/RandomRuins/SpaceRuins/meatpackers.dmm
+++ b/_maps/map_files/RandomRuins/SpaceRuins/meatpackers.dmm
@@ -1277,10 +1277,6 @@
icon_state = "0-2"
},
/obj/structure/cable,
-/obj/structure/cable{
- d2 = 2;
- icon_state = "0-2"
- },
/turf/simulated/floor/plating,
/area/ruin/unpowered/BMPship/Aft)
"eg" = (
@@ -1602,7 +1598,6 @@
outputting = 0
},
/obj/structure/cable,
-/obj/structure/cable,
/turf/simulated/floor/plating,
/area/ruin/unpowered/BMPship/Aft)
"fb" = (
diff --git a/_maps/map_files/RandomRuins/SpaceRuins/onehalf.dmm b/_maps/map_files/RandomRuins/SpaceRuins/onehalf.dmm
index 89dd8083a44..9cca10a3239 100644
--- a/_maps/map_files/RandomRuins/SpaceRuins/onehalf.dmm
+++ b/_maps/map_files/RandomRuins/SpaceRuins/onehalf.dmm
@@ -1136,20 +1136,6 @@
/obj/item/storage/firstaid,
/turf/simulated/floor/plasteel,
/area/ruin/space/onehalf/abandonedbridge)
-"df" = (
-/obj/structure/lattice,
-/obj/structure/cable{
- d1 = 1;
- d2 = 2;
- icon_state = "1-2"
- },
-/obj/structure/cable{
- d1 = 1;
- d2 = 2;
- icon_state = "1-2"
- },
-/turf/template_noop,
-/area/space/nearstation)
"dg" = (
/obj/structure/girder/reinforced,
/obj/item/stack/sheet/plasteel,
@@ -1190,26 +1176,6 @@
/obj/effect/spawner/window/reinforced/grilled,
/turf/simulated/floor/plating,
/area/ruin/space/onehalf/abandonedbridge)
-"dm" = (
-/obj/structure/cable{
- d1 = 4;
- d2 = 8;
- icon_state = "4-8";
- tag = ""
- },
-/obj/structure/cable{
- d1 = 4;
- d2 = 8;
- icon_state = "4-8";
- tag = ""
- },
-/obj/machinery/door/poddoor/preopen{
- id_tag = "onehalf bridge";
- name = "bridge blast door"
- },
-/obj/effect/spawner/window/reinforced/grilled,
-/turf/simulated/floor/plating,
-/area/ruin/space/onehalf/abandonedbridge)
"dn" = (
/obj/effect/spawner/window/reinforced/grilled,
/obj/structure/cable{
@@ -1796,7 +1762,7 @@ Ab
cr
cW
ch
-dm
+dl
ac
aa
"}
@@ -1905,7 +1871,7 @@ ai
ai
ai
cZ
-df
+ai
ai
af
aa
diff --git a/_maps/map_files/RandomRuins/SpaceRuins/ussp.dmm b/_maps/map_files/RandomRuins/SpaceRuins/ussp.dmm
index c0a150801e6..f6febf65964 100644
--- a/_maps/map_files/RandomRuins/SpaceRuins/ussp.dmm
+++ b/_maps/map_files/RandomRuins/SpaceRuins/ussp.dmm
@@ -3010,20 +3010,6 @@
},
/turf/simulated/floor/plasteel/grimy,
/area/ruin/space/derelict/crew_quarters)
-"hp" = (
-/obj/machinery/power/solar,
-/obj/structure/cable{
- d2 = 4;
- icon_state = "0-4"
- },
-/obj/structure/cable{
- d2 = 4;
- icon_state = "0-4"
- },
-/turf/simulated/floor/plasteel/airless{
- icon_state = "solarpanel"
- },
-/area/ruin/space/derelict/solar_control)
"hq" = (
/obj/item/stack/ore/uranium,
/turf/template_noop,
@@ -7502,7 +7488,6 @@
"sp" = (
/obj/machinery/power/solar/fake,
/obj/structure/cable,
-/obj/structure/cable,
/turf/simulated/floor/plasteel/airless{
icon_state = "solarpanel"
},
@@ -7537,7 +7522,6 @@
/turf/template_noop,
/area/ruin/space/derelict/solar_control)
"st" = (
-/obj/structure/cable,
/obj/machinery/power/tracker,
/obj/structure/cable,
/turf/simulated/floor/plasteel/airless{
@@ -7575,6 +7559,13 @@
},
/turf/simulated/wall/mineral/titanium/nodecon/nodiagonal,
/area/ruin/space/derelict/arrival)
+"NK" = (
+/obj/structure/cable{
+ d2 = 8;
+ icon_state = "0-8"
+ },
+/turf/template_noop,
+/area/template_noop)
"Oh" = (
/obj/effect/spawner/airlock/w_to_e{
req_access_txt = 0
@@ -14166,7 +14157,7 @@ ac
ac
ac
ac
-ac
+NK
ac
ac
ac
@@ -14356,18 +14347,18 @@ ac
ac
ac
ac
-hp
-hp
-hp
-hp
-hp
+hs
+hs
+hs
+hs
+hs
ac
jO
ac
jO
ag
hs
-hp
+hs
hs
hs
hs
diff --git a/_maps/map_files/generic/Lavaland.dmm b/_maps/map_files/generic/Lavaland.dmm
index 9769e9b96be..03e670f9f51 100644
--- a/_maps/map_files/generic/Lavaland.dmm
+++ b/_maps/map_files/generic/Lavaland.dmm
@@ -788,11 +788,6 @@
},
/area/mine/laborcamp/security)
"cz" = (
-/obj/structure/cable{
- d1 = 1;
- d2 = 2;
- icon_state = "1-2"
- },
/obj/structure/cable{
d1 = 1;
d2 = 2;
diff --git a/code/_globalvars/lists/misc_lists.dm b/code/_globalvars/lists/misc_lists.dm
index b4ac32d640c..f12c6a8271f 100644
--- a/code/_globalvars/lists/misc_lists.dm
+++ b/code/_globalvars/lists/misc_lists.dm
@@ -67,3 +67,6 @@ GLOBAL_LIST_EMPTY(looping_sounds)
/// List of possible crits from things tipping over
GLOBAL_LIST_EMPTY(tilt_crits)
+
+/// Stores a list of the cached seed icons. Used in the seed extractor and the gene modder
+GLOBAL_LIST_EMPTY(seeds_cached_base64_icons)
diff --git a/code/game/machinery/doors/airlock.dm b/code/game/machinery/doors/airlock.dm
index a51f7aadade..2f2c4ce3229 100644
--- a/code/game/machinery/doors/airlock.dm
+++ b/code/game/machinery/doors/airlock.dm
@@ -1552,10 +1552,12 @@ GLOBAL_LIST_EMPTY(airlock_emissive_underlays)
ae.selected_accesses = req_one_access
ae.one_access = 1
ae.unres_access_from = unres_sides
+ ae.is_installed = FALSE
else
ae = electronics
electronics = null
ae.forceMove(loc)
+ ae.is_installed = FALSE
QDEL_LIST_CONTENTS(fillers)
qdel(src)
diff --git a/code/game/machinery/doors/airlock_electronics.dm b/code/game/machinery/doors/airlock_electronics.dm
index 070c29e377d..3b9da5c1be5 100644
--- a/code/game/machinery/doors/airlock_electronics.dm
+++ b/code/game/machinery/doors/airlock_electronics.dm
@@ -18,6 +18,8 @@
var/const/max_brain_damage = 60
/// Which direction has unrestricted access to the airlock (e.g. medbay doors from the inside)
var/unres_access_from = null
+ /// Is this electronic installed in a door?
+ var/is_installed = FALSE
/obj/item/airlock_electronics/Initialize(mapload)
. = ..()
@@ -38,7 +40,6 @@
return
ui_interact(user)
-
// tgui\packages\tgui\interfaces\AirlockElectronics.js
/obj/item/airlock_electronics/ui_state(mob/user)
return GLOB.inventory_state
diff --git a/code/game/objects/items/weapons/bio_chips/bio_chip_pad.dm b/code/game/objects/items/weapons/bio_chips/bio_chip_pad.dm
index e0867c56652..8ddc1d8cfb3 100644
--- a/code/game/objects/items/weapons/bio_chips/bio_chip_pad.dm
+++ b/code/game/objects/items/weapons/bio_chips/bio_chip_pad.dm
@@ -9,6 +9,7 @@
w_class = WEIGHT_CLASS_SMALL
var/obj/item/bio_chip_case/case
+ var/static/list/cached_base64_icons = list()
/obj/item/bio_chip_pad/Destroy()
if(case)
@@ -80,6 +81,10 @@
data["contains_case"] = case ? TRUE : FALSE
if(case && case.imp)
var/datum/implant_fluff/implant_data = case.imp.implant_data
+ var/icon/base64icon = cached_base64_icons["[initial(case.imp.icon)][initial(case.imp.icon_state)]"]
+ if(!base64icon)
+ base64icon = "[icon2base64(icon(initial(case.imp.icon), initial(case.imp.icon_state), SOUTH, 1))]"
+ cached_base64_icons["[initial(case.imp.icon)][initial(case.imp.icon_state)]"] = base64icon
data["implant"] = list(
"name" = implant_data.name,
"life" = implant_data.life,
diff --git a/code/game/objects/structures/displaycase.dm b/code/game/objects/structures/displaycase.dm
index bf94750fbae..4a11d6cc15d 100644
--- a/code/game/objects/structures/displaycase.dm
+++ b/code/game/objects/structures/displaycase.dm
@@ -207,10 +207,12 @@
to_chat(user, "You start installing the electronics into [src]...")
playsound(loc, I.usesound, 50, 1)
if(do_after(user, 30, target = src))
- if(user.drop_item())
- I.forceMove(src)
- electronics = I
+ var/obj/item/airlock_electronics/new_electronics = I
+ if(user.drop_item() && !new_electronics.is_installed)
+ new_electronics.forceMove(src)
+ electronics = new_electronics
to_chat(user, "You install the airlock electronics.")
+ electronics.is_installed = TRUE
else if(istype(I, /obj/item/stack/sheet/glass))
var/obj/item/stack/sheet/glass/G = I
@@ -241,6 +243,7 @@
if(I.use_tool(src, user, 0, volume = I.tool_volume))
to_chat(user, "You remove the airlock electronics.")
new /obj/item/airlock_electronics(drop_location(), 1)
+ electronics.is_installed = FALSE
electronics = null
/obj/structure/displaycase_chassis/wrench_act(mob/user, obj/item/I)
diff --git a/code/game/objects/structures/door_assembly.dm b/code/game/objects/structures/door_assembly.dm
index fdfe4194b5a..6e3bcbd9171 100644
--- a/code/game/objects/structures/door_assembly.dm
+++ b/code/game/objects/structures/door_assembly.dm
@@ -82,14 +82,16 @@
user.visible_message("[user] installs the electronics into the airlock assembly.", "You start to install electronics into the airlock assembly...")
if(do_after(user, 40 * W.toolspeed, target = src))
- if(state != AIRLOCK_ASSEMBLY_NEEDS_ELECTRONICS)
+ var/obj/item/airlock_electronics/new_electronics = W
+ if(state != AIRLOCK_ASSEMBLY_NEEDS_ELECTRONICS || new_electronics.is_installed)
return
user.drop_item()
- W.forceMove(src)
+ new_electronics.forceMove(src)
to_chat(user, "You install the airlock electronics.")
state = AIRLOCK_ASSEMBLY_NEEDS_SCREWDRIVER
name = "near finished airlock assembly"
- electronics = W
+ electronics = new_electronics
+ electronics.is_installed = TRUE
else if(istype(W, /obj/item/stack/sheet) && (!glass || !mineral))
var/obj/item/stack/sheet/S = W
@@ -137,6 +139,7 @@
ae = electronics
electronics = null
ae.forceMove(loc)
+ ae.is_installed = FALSE
update_appearance(UPDATE_NAME | UPDATE_OVERLAYS)
/obj/structure/door_assembly/screwdriver_act(mob/user, obj/item/I)
diff --git a/code/game/objects/structures/windoor_assembly.dm b/code/game/objects/structures/windoor_assembly.dm
index 09f57dede18..6c34c677f50 100644
--- a/code/game/objects/structures/windoor_assembly.dm
+++ b/code/game/objects/structures/windoor_assembly.dm
@@ -155,16 +155,18 @@
user.visible_message("[user] installs the electronics into the windoor assembly.", "You start to install electronics into the windoor assembly...")
user.drop_item()
W.forceMove(src)
+ var/obj/item/airlock_electronics/new_electronics = W
- if(do_after(user, 40 * W.toolspeed, target = src))
+ if(do_after(user, 40 * new_electronics.toolspeed, target = src) && !new_electronics.is_installed)
if(!src || electronics)
- W.forceMove(loc)
+ new_electronics.forceMove(loc)
return
to_chat(user, "You install the windoor electronics.")
name = "near finished windoor assembly"
- electronics = W
+ electronics = new_electronics
+ electronics.is_installed = TRUE
else
- W.forceMove(loc)
+ new_electronics.forceMove(loc)
else if(is_pen(W))
var/t = rename_interactive(user, W)
@@ -247,6 +249,7 @@
ae = electronics
electronics = null
ae.forceMove(loc)
+ ae.is_installed = FALSE
/obj/structure/windoor_assembly/wirecutter_act(mob/user, obj/item/I)
if(state != WIRED_ASSEMBLY)
diff --git a/code/modules/admin/verbs/adminpm.dm b/code/modules/admin/verbs/adminpm.dm
index cf562a6c90b..4da858daca0 100644
--- a/code/modules/admin/verbs/adminpm.dm
+++ b/code/modules/admin/verbs/adminpm.dm
@@ -120,7 +120,7 @@
var/datum/controller/subsystem/tickets/tickets_system
// We treat PMs as mentorhelps if we were explicitly so, or if neither
// party is an admin.
- if(type == "Mentorhelp" || !(check_rights(R_ADMIN|R_MOD, 0, C.mob) || check_rights(R_ADMIN|R_MOD, 0, src.mob)))
+ if(type == "Mentorhelp" || !(check_rights(R_ADMIN|R_MOD, 0, C.mob) || check_rights(R_ADMIN|R_MOD, 0, mob)))
send_span = "mentorhelp"
recieve_span = "mentorhelp"
message_type = MESSAGE_TYPE_MENTORPM
@@ -167,22 +167,32 @@
return
var/ping_link = check_rights(R_ADMIN, 0, mob) ? "(PING)" : ""
- var/window_link = "(WINDOW)"
+ var/ticket_link
var/alert_link = check_rights(R_ADMIN, FALSE, mob) ? "(ALERT)" : ""
if(ticket_id != -1)
if(message_type == MESSAGE_TYPE_MENTORPM)
- window_link = "(TICKET)"
+ ticket_link = "(TICKET)"
else
- window_link = "(TICKET)"
+ ticket_link = "(TICKET)"
var/emoji_msg = "[msg]"
- recieve_message = "[type] from-[recieve_pm_type] [C.holder ? key_name(src, TRUE, type, ticket_id = ticket_id) : key_name_hidden(src, TRUE, type, ticket_id = ticket_id)]:
[emoji_msg][C.holder ? "
[ping_link] [window_link] [alert_link]" : ""]"
+ var/recieve_window_link = "(WINDOW)"
+ if(message_type == MESSAGE_TYPE_MENTORPM && check_rights(R_ADMIN|R_MENTOR, 0, C.mob))
+ recieve_window_link = ticket_link
+ else if(message_type == MESSAGE_TYPE_ADMINPM && check_rights(R_ADMIN, 0, C.mob))
+ recieve_window_link = ticket_link
+ recieve_message = "[type] from-[recieve_pm_type] [C.holder ? key_name(src, TRUE, type, ticket_id = ticket_id) : key_name_hidden(src, TRUE, type, ticket_id = ticket_id)]:
[emoji_msg][C.holder ? "
[ping_link] [recieve_window_link] [alert_link]" : ""]"
if(message_type == MESSAGE_TYPE_MENTORPM)
recieve_message = chat_box_mhelp(recieve_message)
else
recieve_message = chat_box_ahelp(recieve_message)
to_chat(C, recieve_message)
- var/send_message = "[send_pm_type][type] to-[holder ? key_name(C, TRUE, type, ticket_id = ticket_id) : key_name_hidden(C, TRUE, type, ticket_id = ticket_id)]:
[emoji_msg]
[ping_link] [window_link] [alert_link]"
+ var/send_window_link = "(WINDOW)"
+ if(message_type == MESSAGE_TYPE_MENTORPM && check_rights(R_ADMIN|R_MENTOR, 0, mob))
+ send_window_link = ticket_link
+ else if(message_type == MESSAGE_TYPE_ADMINPM && check_rights(R_ADMIN, 0, mob))
+ send_window_link = ticket_link
+ var/send_message = "[send_pm_type][type] to-[holder ? key_name(C, TRUE, type, ticket_id = ticket_id) : key_name_hidden(C, TRUE, type, ticket_id = ticket_id)]:
[emoji_msg]
[ping_link] [send_window_link] [alert_link]"
if(message_type == MESSAGE_TYPE_MENTORPM)
send_message = chat_box_mhelp(send_message)
else
@@ -191,9 +201,9 @@
var/third_party_message
if(message_type == MESSAGE_TYPE_MENTORPM)
- third_party_message = chat_box_mhelp("[type]: [key_name(src, TRUE, type, ticket_id = ticket_id)]->[key_name(C, TRUE, type, ticket_id = ticket_id)]:
[emoji_msg]
[ping_link] [window_link] [alert_link]")
+ third_party_message = chat_box_mhelp("[type]: [key_name(src, TRUE, type, ticket_id = ticket_id)]->[key_name(C, TRUE, type, ticket_id = ticket_id)]:
[emoji_msg]
[ping_link] [ticket_link] [alert_link]")
else
- third_party_message = chat_box_ahelp("[type]: [key_name(src, TRUE, type, ticket_id = ticket_id)]->[key_name(C, TRUE, type, ticket_id = ticket_id)]:
[emoji_msg]
[ping_link] [window_link] [alert_link]")
+ third_party_message = chat_box_ahelp("[type]: [key_name(src, TRUE, type, ticket_id = ticket_id)]->[key_name(C, TRUE, type, ticket_id = ticket_id)]:
[emoji_msg]
[ping_link] [ticket_link] [alert_link]")
//play the recieving admin the adminhelp sound (if they have them enabled)
//non-admins always hear the sound, as they cannot toggle it
@@ -287,6 +297,7 @@
target.pm_tracker.show_ui(target.mob)
/datum/pm_tracker
+ var/ckey
var/current_title = ""
var/open = FALSE
var/list/datum/pm_convo/pms = list()
@@ -294,6 +305,9 @@
var/window_id = "pms_window"
var/forced = FALSE
+/datum/pm_tracker/New(ckey_in)
+ ckey = ckey_in
+
/datum/pm_convo
var/list/messages = list()
var/archived = FALSE
@@ -326,6 +340,10 @@
show_ui(user)
/datum/pm_tracker/proc/show_ui(mob/user)
+ // Please do not open someone else's PMs, that makes them not very private.
+ if(user.ckey != ckey)
+ return
+
var/dat = ""
// If it was forced open, make them use a special close button that alerts admins to closure
diff --git a/code/modules/client/client_defines.dm b/code/modules/client/client_defines.dm
index 31edadc979f..aa06fa2055c 100644
--- a/code/modules/client/client_defines.dm
+++ b/code/modules/client/client_defines.dm
@@ -11,7 +11,7 @@
var/last_message = "" //contains the last message sent by this client - used to protect against copy-paste spamming.
var/last_message_count = 0 //contains a number of how many times a message identical to last_message was sent.
var/last_message_time = 0 //holds the last time (based on world.time) a message was sent
- var/datum/pm_tracker/pm_tracker = new()
+ var/datum/pm_tracker/pm_tracker
/////////
//OTHER//
diff --git a/code/modules/client/client_procs.dm b/code/modules/client/client_procs.dm
index d2412ed6d54..0ad98f3ae25 100644
--- a/code/modules/client/client_procs.dm
+++ b/code/modules/client/client_procs.dm
@@ -262,6 +262,9 @@
stat_panel = new(src, "statbrowser")
stat_panel.subscribe(src, PROC_REF(on_stat_panel_message))
+ // Create a PM tracker bound to this ckey.
+ pm_tracker = new(ckey)
+
tgui_panel = new(src, "browseroutput")
tgui_say = new(src, "tgui_say")
TopicData = null //Prevent calls to client.Topic from connect
diff --git a/code/modules/games/cards.dm b/code/modules/games/cards.dm
index ad5e29ab160..83d32b64ac4 100644
--- a/code/modules/games/cards.dm
+++ b/code/modules/games/cards.dm
@@ -50,10 +50,16 @@
/obj/item/deck/Initialize(mapload)
. = ..()
+ build_decks()
+ update_icon(UPDATE_ICON_STATE)
+
+/obj/item/deck/proc/build_decks()
+ if(length(cards))
+ // prevent building decks more than once
+ return
for(var/deck in 1 to deck_size)
build_deck()
deck_total = length(cards)
- update_icon(UPDATE_ICON_STATE)
/obj/item/deck/proc/build_deck()
return
@@ -227,8 +233,10 @@
var/mob/living/user = usr
if(cooldown < world.time - 1 SECONDS)
cards = shuffle(cards)
- user.visible_message("[user] shuffles [src].")
- playsound(user, 'sound/items/cardshuffle.ogg', 50, 1)
+
+ if(user)
+ user.visible_message("[user] shuffles [src].")
+ playsound(user, 'sound/items/cardshuffle.ogg', 50, TRUE)
cooldown = world.time
diff --git a/code/modules/hydroponics/gene_modder.dm b/code/modules/hydroponics/gene_modder.dm
index 7d10426d9ed..cf66b5f57f8 100644
--- a/code/modules/hydroponics/gene_modder.dm
+++ b/code/modules/hydroponics/gene_modder.dm
@@ -167,8 +167,12 @@
data["seed"] = list()
if(seed)
+ var/icon/base64icon = GLOB.seeds_cached_base64_icons["[initial(seed.icon)][initial(seed.icon_state)]"]
+ if(!base64icon)
+ base64icon = icon2base64(icon(initial(seed.icon), initial(seed.icon_state), SOUTH, 1))
+ GLOB.seeds_cached_base64_icons["[initial(seed.icon)][initial(seed.icon_state)]"] = base64icon
data["seed"] = list(
- "image" = "[icon2base64(icon(initial(seed.icon), initial(seed.icon_state), SOUTH, 1))]",
+ "image" = "[base64icon]",
"name" = seed.name,
"variant" = seed.variant
)
diff --git a/code/modules/hydroponics/seed_extractor.dm b/code/modules/hydroponics/seed_extractor.dm
index 07616d4ae69..285882c39f9 100644
--- a/code/modules/hydroponics/seed_extractor.dm
+++ b/code/modules/hydroponics/seed_extractor.dm
@@ -136,8 +136,12 @@
for(var/datum/seed_pile/O in piles)
var/obj/item/I = O.path
+ var/icon/base64icon = GLOB.seeds_cached_base64_icons["[initial(I.icon)][initial(I.icon_state)]"]
+ if(!base64icon)
+ base64icon = icon2base64(icon(initial(I.icon), initial(I.icon_state), SOUTH, 1))
+ GLOB.seeds_cached_base64_icons["[initial(I.icon)][initial(I.icon_state)]"] = base64icon
var/list/seed_info = list(
- "image" = "[icon2base64(icon(initial(I.icon), initial(I.icon_state), SOUTH, 1))]",
+ "image" = "[base64icon]",
"id" = O.id,
"name" = O.name,
"variant" = O.variant,
diff --git a/code/modules/projectiles/gun.dm b/code/modules/projectiles/gun.dm
index ed3abfcfec5..6b52313e991 100644
--- a/code/modules/projectiles/gun.dm
+++ b/code/modules/projectiles/gun.dm
@@ -171,13 +171,12 @@
if(flag)
if(user.zone_selected == "mouth")
- if(HAS_TRAIT(user, TRAIT_BADASS))
+ if(target == user && HAS_TRAIT(user, TRAIT_BADASS)) // Check if we are blowing smoke off of our own gun, otherwise we are trying to execute someone
user.visible_message("[user] blows smoke off of [src]'s barrel. What a badass.")
else
handle_suicide(user, target, params)
return
-
//Exclude lasertag guns from the CLUMSY check.
if(clumsy_check)
if(istype(user))
diff --git a/code/modules/unit_tests/_unit_tests.dm b/code/modules/unit_tests/_unit_tests.dm
index 7f234307d41..76f2504ef03 100644
--- a/code/modules/unit_tests/_unit_tests.dm
+++ b/code/modules/unit_tests/_unit_tests.dm
@@ -2,6 +2,7 @@
//Keep this sorted alphabetically
#ifdef UNIT_TESTS
+#include "games\test_cards.dm"
#include "jobs\test_job_globals.dm"
#include "aicard_icons.dm"
#include "announcements.dm"
diff --git a/code/modules/unit_tests/games/test_cards.dm b/code/modules/unit_tests/games/test_cards.dm
new file mode 100644
index 00000000000..bde0b9f1f8b
--- /dev/null
+++ b/code/modules/unit_tests/games/test_cards.dm
@@ -0,0 +1,30 @@
+/datum/unit_test/card_deck/proc/validate_deck(obj/item/deck/deck)
+ var/list/card_count = list()
+ for(var/datum/playingcard/card in deck.cards)
+ if(card_count[card.name] == null)
+ card_count[card.name] = 1
+ else if(card.name == "Joker")
+ card_count[card.name]++
+ else
+ // duplicate card in deck
+ return FALSE
+
+ if(length(card_count) != 53) // 2 Jokers, so 53 unique cards
+ return FALSE
+ return TRUE
+
+
+/datum/unit_test/card_deck/Run()
+ // setup
+ var/loc = pick(block(run_loc_bottom_left, run_loc_top_right))
+ var/obj/item/deck/cards/cards = new /obj/item/deck/cards(loc)
+ cards.build_decks()
+
+ // is deck proper upon spawning
+ if(!validate_deck(cards))
+ Fail("52 card deck not initialized correctly.")
+
+ // is deck proper after shuffling
+ cards.deckshuffle()
+ if(!validate_deck(cards))
+ Fail("52 card deck broken after shuffling.")
diff --git a/tools/maplint/lints/stacked_matching_cables.yml b/tools/maplint/lints/stacked_matching_cables.yml
new file mode 100644
index 00000000000..6b041026257
--- /dev/null
+++ b/tools/maplint/lints/stacked_matching_cables.yml
@@ -0,0 +1,4 @@
+/obj/structure/cable:
+ banned_neighbors:
+ /obj/structure/cable:
+ identical: true
diff --git a/tools/maplint/source/lint.py b/tools/maplint/source/lint.py
index 4af9ad5c1cf..ee86d29be52 100644
--- a/tools/maplint/source/lint.py
+++ b/tools/maplint/source/lint.py
@@ -66,7 +66,13 @@ class BannedNeighbor:
def matches(self, identified: Content, neighbor: Content):
if self.identical:
- return neighbor == identified
+ if identified.path != neighbor.path:
+ return False
+
+ if identified.var_edits != neighbor.var_edits:
+ return False
+
+ return True
if self.typepath is not None:
if self.typepath.matches_path(neighbor.path):