diff --git a/code/__DEFINES/bodyparts.dm b/code/__DEFINES/bodyparts.dm
index 19af74fe43db2..f044bb12c84ea 100644
--- a/code/__DEFINES/bodyparts.dm
+++ b/code/__DEFINES/bodyparts.dm
@@ -47,3 +47,5 @@
// Color priorities for bodyparts
#define LIMB_COLOR_HULK 10
#define LIMB_COLOR_CARP_INFUSION 20
+/// Base priority for atom colors, gets atom priorities added to it
+#define LIMB_COLOR_ATOM_COLOR 30
diff --git a/code/__DEFINES/colors.dm b/code/__DEFINES/colors.dm
index 6e9af2cdb9929..823f49b389259 100644
--- a/code/__DEFINES/colors.dm
+++ b/code/__DEFINES/colors.dm
@@ -468,6 +468,8 @@ GLOBAL_LIST_INIT(heretic_path_to_color, list(
// Lowest priority
#define EYE_COLOR_ORGAN_PRIORITY 1
+/// Base priority for atom colors, gets atom priorities added to it
+#define EYE_COLOR_ATOM_COLOR_PRIORITY 2
#define EYE_COLOR_SPECIES_PRIORITY 10
#define EYE_COLOR_WEED_PRIORITY 20
#define EYE_COLOR_CULT_PRIORITY 30
diff --git a/code/controllers/subsystem/persistence/trophy_fishes.dm b/code/controllers/subsystem/persistence/trophy_fishes.dm
index 62fe8dfdfa090..e7e0b635a2b8a 100644
--- a/code/controllers/subsystem/persistence/trophy_fishes.dm
+++ b/code/controllers/subsystem/persistence/trophy_fishes.dm
@@ -32,10 +32,9 @@
fish.set_custom_materials(mat_list)
fish.persistence_load(data)
fish.name = data[PERSISTENCE_FISH_NAME]
- mount.catcher_name = data[PERSISTENCE_FISH_CATCHER]
- mount.catch_date = data[PERSISTENCE_FISH_CATCH_DATE]
fish.set_status(FISH_DEAD, silent = TRUE)
mount.add_fish(fish, from_persistence = TRUE, catcher = data[PERSISTENCE_FISH_CATCHER])
+ mount.catch_date = data[PERSISTENCE_FISH_CATCH_DATE]
/datum/controller/subsystem/persistence/proc/save_trophy_fish(obj/structure/fish_mount/mount)
var/obj/item/fish/fish = mount.mounted_fish
diff --git a/code/controllers/subsystem/polling.dm b/code/controllers/subsystem/polling.dm
index 6624c984cbb6a..a68ff090a8d91 100644
--- a/code/controllers/subsystem/polling.dm
+++ b/code/controllers/subsystem/polling.dm
@@ -155,7 +155,7 @@ SUBSYSTEM_DEF(polling)
act_never = "[custom_link_style_start]\[Never For This Round\]"
if(!duplicate_message_check(alert_poll)) //Only notify people once. They'll notice if there are multiple and we don't want to spam people.
- SEND_SOUND(candidate_mob, 'sound/announcer/notice/notice2.ogg')
+ SEND_SOUND(candidate_mob, sound('sound/misc/prompt.ogg', volume = 70))
var/surrounding_icon
if(chat_text_border_icon)
var/image/surrounding_image
diff --git a/code/datums/ai_laws/ai_laws.dm b/code/datums/ai_laws/ai_laws.dm
index a25f7e694a9ad..b992cb56cd2c7 100644
--- a/code/datums/ai_laws/ai_laws.dm
+++ b/code/datums/ai_laws/ai_laws.dm
@@ -79,8 +79,15 @@ GLOBAL_VAR(round_default_lawset)
/proc/pick_weighted_lawset()
var/datum/ai_laws/lawtype
var/list/law_weights = CONFIG_GET(keyed_list/law_weight)
+ var/list/specified_law_ids = CONFIG_GET(keyed_list/specified_laws)
+
if(HAS_TRAIT(SSstation, STATION_TRAIT_UNIQUE_AI))
- law_weights -= AI_LAWS_ASIMOV
+ switch(CONFIG_GET(number/default_laws))
+ if(CONFIG_ASIMOV)
+ law_weights -= AI_LAWS_ASIMOV
+ if(CONFIG_CUSTOM)
+ law_weights -= specified_law_ids
+
while(!lawtype && law_weights.len)
var/possible_id = pick_weight(law_weights)
lawtype = lawid_to_type(possible_id)
diff --git a/code/datums/elements/decals/blood.dm b/code/datums/elements/decals/blood.dm
index 16fd4241147d4..3d16df0c61283 100644
--- a/code/datums/elements/decals/blood.dm
+++ b/code/datums/elements/decals/blood.dm
@@ -26,7 +26,7 @@
var/icon/icon_for_size = icon(icon, icon_state)
var/scale_factor_x = icon_for_size.Width()/ICON_SIZE_X
var/scale_factor_y = icon_for_size.Height()/ICON_SIZE_Y
- var/mutable_appearance/blood_splatter = mutable_appearance('icons/effects/blood.dmi', "itemblood", appearance_flags = RESET_COLOR) //MA of the blood that we apply
+ var/mutable_appearance/blood_splatter = mutable_appearance('icons/effects/blood.dmi', "itemblood", appearance_flags = KEEP_APART|RESET_COLOR) //MA of the blood that we apply
blood_splatter.transform = blood_splatter.transform.Scale(scale_factor_x, scale_factor_y)
blood_splatter.blend_mode = BLEND_INSET_OVERLAY
blood_splatter.color = _color
diff --git a/code/game/objects/effects/spawners/random/trash.dm b/code/game/objects/effects/spawners/random/trash.dm
index 6f6f5badc8e7e..a6d9bfcc45afc 100644
--- a/code/game/objects/effects/spawners/random/trash.dm
+++ b/code/game/objects/effects/spawners/random/trash.dm
@@ -53,6 +53,12 @@
/obj/effect/spawner/random/entertainment/cigar = 1,
/obj/item/stack/ore/gold = 1,
)
+/obj/effect/spawner/random/trash/deluxe_garbage/Initialize(mapload)
+ if(mapload)
+ var/turf/location = get_turf(loc)
+ if(location.initial_gas_mix != OPENTURF_DEFAULT_ATMOS && location.initial_gas_mix != OPENTURF_DIRTY_ATMOS)
+ loot -= /mob/living/basic/mouse
+ return ..()
/obj/effect/spawner/random/trash/cigbutt
name = "cigarette butt spawner"
diff --git a/code/game/objects/items/crayons.dm b/code/game/objects/items/crayons.dm
index 18da163bce4a6..27103d74ac5cc 100644
--- a/code/game/objects/items/crayons.dm
+++ b/code/game/objects/items/crayons.dm
@@ -829,9 +829,7 @@
if(isbodypart(target))
var/obj/item/bodypart/limb = target
if(IS_ROBOTIC_LIMB(limb))
- context[SCREENTIP_CONTEXT_CTRL_LMB] = "Restyle robotic limb"
- else
- context[SCREENTIP_CONTEXT_CTRL_LMB] = "Copy color"
+ context[SCREENTIP_CONTEXT_LMB] = "Restyle robotic limb"
return CONTEXTUAL_SCREENTIP_SET
@@ -887,9 +885,6 @@
return ..()
/obj/item/toy/crayon/spraycan/use_on(atom/target, mob/user, list/modifiers)
- if (LAZYACCESS(modifiers, CTRL_CLICK))
- return ctrl_interact(target, user)
-
if(is_capped)
balloon_alert(user, "take the cap off first!")
return ITEM_INTERACT_BLOCKING
@@ -897,6 +892,10 @@
if(check_empty(user))
return ITEM_INTERACT_BLOCKING
+ if (isbodypart(target))
+ if (color_limb(target, user))
+ return ITEM_INTERACT_SUCCESS
+
if(iscarbon(target))
if(pre_noise || post_noise)
playsound(user.loc, 'sound/effects/spray.ogg', 25, TRUE, 5)
@@ -994,30 +993,9 @@
user.visible_message(span_notice("[user] coats [target] with spray paint!"), span_notice("You coat [target] with spray paint."))
return ITEM_INTERACT_SUCCESS
-/obj/item/toy/crayon/spraycan/proc/ctrl_interact(atom/interacting_with, mob/living/user)
- if(is_capped)
- if(!interacting_with.color)
- // let's be generous and assume if they're trying to match something with no color, while capped,
- // we shouldn't be blocking further interactions
- return NONE
- balloon_alert(user, "take the cap off first!")
- return ITEM_INTERACT_BLOCKING
-
- if(check_empty(user))
- return ITEM_INTERACT_BLOCKING
-
- if(!isbodypart(interacting_with) || !actually_paints)
- if(interacting_with.color)
- paint_color = interacting_with.color
- balloon_alert(user, "matched colour of target")
- update_appearance()
- return ITEM_INTERACT_BLOCKING
- balloon_alert(user, "can't match those colours!")
- return ITEM_INTERACT_BLOCKING
-
- var/obj/item/bodypart/limb = interacting_with
+/obj/item/toy/crayon/spraycan/proc/color_limb(obj/item/bodypart/limb, mob/living/user)
if(!IS_ROBOTIC_LIMB(limb))
- return ITEM_INTERACT_BLOCKING
+ return FALSE
var/list/skins = list()
var/static/list/style_list_icons = list(
@@ -1036,7 +1014,7 @@
if(choice && (use_charges(user, 5, requires_full = FALSE)))
playsound(user.loc, 'sound/effects/spray.ogg', 5, TRUE, 5)
limb.change_appearance(style_list_icons[choice], greyscale = FALSE)
- return ITEM_INTERACT_SUCCESS
+ return TRUE
/obj/item/toy/crayon/spraycan/click_alt(mob/user)
if(!has_cap)
diff --git a/code/game/objects/items/forensicsspoofer.dm b/code/game/objects/items/forensicsspoofer.dm
new file mode 100644
index 0000000000000..b2384d2dda237
--- /dev/null
+++ b/code/game/objects/items/forensicsspoofer.dm
@@ -0,0 +1,198 @@
+/obj/item/forensics_spoofer
+ name = /obj/item/detective_scanner::name
+ desc = "Used to adjacently scan objects and biomass for fibers and fingerprints. Can replicate the findings."
+ icon = /obj/item/detective_scanner::icon
+ icon_state = /obj/item/detective_scanner::icon_state
+ w_class = WEIGHT_CLASS_SMALL
+ inhand_icon_state = /obj/item/detective_scanner::inhand_icon_state
+ worn_icon_state = /obj/item/detective_scanner::worn_icon_state
+ lefthand_file = /obj/item/detective_scanner::lefthand_file
+ righthand_file = /obj/item/detective_scanner::righthand_file
+ obj_flags = CONDUCTS_ELECTRICITY
+ item_flags = NOBLUDGEON
+ slot_flags = ITEM_SLOT_BELT
+ /// stored fibers in memory
+ var/list/fibers = list()
+ /// stored fingerprints in memory
+ var/list/fingerprints = list()
+ /// chosen fiber to add to target
+ var/chosen_fiber
+ /// chosen fingerprint to add to target
+ var/chosen_fingerprint
+ /// max storage for fibers/fingerprints seperate for each
+ var/max_storage = 5
+ /// do we scan for new material? if false will tamper
+ var/scan_mode = TRUE
+ /// do we make forensics scanner messages and sounds
+ var/silent_mode = FALSE
+ /// tamper cooldown time so people dont spam it on every single wall and thing ever
+ var/tamper_cooldown_time = 1 SECONDS
+ COOLDOWN_DECLARE(tamper_cooldown)
+
+/obj/item/forensics_spoofer/Initialize(mapload)
+ . = ..()
+ // most things have add_fingerprint in their item interaction because lol lmao
+ // tl;dr cut off the chain before anything fires so we dont add user fingerprints to target
+ RegisterSignal(src, COMSIG_ITEM_INTERACTING_WITH_ATOM, PROC_REF(do_interact))
+
+/obj/item/forensics_spoofer/attack_self_secondary(mob/user, modifiers)
+ . = ..()
+ if(.)
+ return
+ scan_mode = !scan_mode
+ balloon_alert(user, "now [scan_mode ? "scanning" : "applying"]")
+ return SECONDARY_ATTACK_CANCEL_ATTACK_CHAIN
+
+// ok due to shenanigans basically every item interact adds your fingerprints to it which isnt ideal so we have this
+/obj/item/forensics_spoofer/proc/do_interact(datum/source, mob/living/user, atom/interacting_with, list/modifiers)
+ SIGNAL_HANDLER
+ if(scan_mode)
+ INVOKE_ASYNC(src, PROC_REF(scan), interacting_with, user)
+ else
+ tamper(interacting_with, user, do_fibers = !isnull(chosen_fiber))
+ return ITEM_INTERACT_SUCCESS
+
+/obj/item/forensics_spoofer/proc/do_fake_scan(atom/target, mob/user)
+ if(silent_mode)
+ return
+ playsound(src, SFX_INDUSTRIAL_SCAN, 20, TRUE, -2, TRUE, FALSE)
+ user.visible_message(
+ span_notice("\The [user] points the [name] at \the [target] and performs a forensic scan.")
+ )
+
+/obj/item/forensics_spoofer/proc/clear_values(list/the_list)
+ for(var/key in the_list)
+ the_list[key] = ""
+
+/obj/item/forensics_spoofer/proc/scan(atom/target, mob/living/user)
+ do_fake_scan(target, user)
+ if(isnull(target.forensics))
+ target.balloon_alert(user, "nothing!")
+ return ITEM_INTERACT_FAILURE
+ var/list/new_fibers = LAZYCOPY(target.forensics.fibers) - fibers
+ var/list/new_prints = LAZYCOPY(target.forensics.fingerprints) - fingerprints
+ var/new_len = length(new_fibers) + length(new_prints)
+ balloon_alert(user, "[new_len ? new_len : "no"] new prints/fibers")
+ if(new_len)
+ var/list/message = list(span_bold("Scan results (Unstored Only):"))
+ for(var/text in new_fibers)
+ message += span_notice("Fiber: [text]")
+ if(length(fibers) > max_storage)
+ message += span_boldwarning("Fiber storage full.")
+ for(var/text in new_prints)
+ message += span_notice("Fingerprint: [text]")
+ if(length(fingerprints) > max_storage)
+ message += span_boldwarning("Fingerprint storage full.")
+ to_chat(user, boxed_message(jointext(message, "\n")), type = MESSAGE_TYPE_INFO)
+ if(length(fingerprints) < max_storage)
+ while(length(fingerprints) + length(new_prints) > max_storage)
+ var/to_remove = tgui_input_list(user, "Too many prints, cancel to discard all", "What to discard", new_fibers)
+ if(isnull(to_remove))
+ return ITEM_INTERACT_FAILURE
+ new_prints -= to_remove
+ clear_values(new_prints)
+ fingerprints += new_prints
+ for(var/fingerprint in fingerprints)
+ fingerprints[fingerprint] = get_name_from_fingerprint(fingerprint)
+ if(length(fibers) < max_storage)
+ while(length(fibers) + length(new_fibers) > max_storage)
+ var/to_remove = tgui_input_list(user, "Too many prints, cancel to discard all", "What to discard", new_fibers)
+ if(isnull(to_remove))
+ return ITEM_INTERACT_FAILURE
+ new_fibers -= to_remove
+ clear_values(new_fibers)
+ fibers += new_fibers
+ return ITEM_INTERACT_SUCCESS
+
+/obj/item/forensics_spoofer/proc/tamper(atom/target, mob/living/user, do_fibers = FALSE)
+ do_fake_scan(target, user)
+ if((!do_fibers && isnull(chosen_fingerprint)) || (do_fibers && isnull(chosen_fiber)))
+ balloon_alert(user, "no [do_fibers ? "fiber" : "fingerprint"] selected!") // we CAN automatically select it but if they dont have it selected then they likely didnt know of it in the first place so they learn it now
+ return ITEM_INTERACT_FAILURE
+ if(!COOLDOWN_FINISHED(src, tamper_cooldown))
+ balloon_alert(user, "please wait!")
+ return ITEM_INTERACT_FAILURE
+ if(!isnull(target.forensics) && LAZYFIND(do_fibers ? target.forensics.fibers : target.forensics.fingerprints, do_fibers ? chosen_fiber : chosen_fingerprint))
+ balloon_alert(user, "already present!")
+ return ITEM_INTERACT_FAILURE
+
+ if(do_fibers)
+ target.add_fiber_list(list(chosen_fiber))
+ user.log_message("has tampered with the fingerprints/fibers of [src]. Added [chosen_fiber]", LOG_ATTACK)
+ else
+ target.add_fingerprint_list(list(chosen_fingerprint))
+ user.log_message("has tampered with the fingerprints/fibers of [src]. Added [chosen_fingerprint]", LOG_ATTACK)
+
+ target.balloon_alert(user, "[do_fibers ? "fiber" : "fingerprint"] added")
+ target.add_hiddenprint(user)
+ COOLDOWN_START(src, tamper_cooldown, tamper_cooldown_time)
+
+ return ITEM_INTERACT_SUCCESS
+
+/obj/item/forensics_spoofer/proc/get_name_from_fingerprint(fingerprint)
+ . = "Unknown"
+ for(var/datum/record/crew/player_record as anything in GLOB.manifest.general)
+ if(player_record.fingerprint != fingerprint)
+ continue
+ return player_record.name
+
+/obj/item/forensics_spoofer/ui_state(mob/user)
+ return GLOB.hands_state
+
+/obj/item/forensics_spoofer/ui_interact(mob/user, datum/tgui/ui)
+ ui = SStgui.try_update_ui(user, src, ui)
+ if(!ui)
+ ui = new(user, src, "ForensicsSpoofer", name)
+ ui.open()
+
+/obj/item/forensics_spoofer/ui_static_data(mob/user)
+ . = list(
+ "max_storage" = max_storage,
+ )
+
+/obj/item/forensics_spoofer/ui_data(mob/user)
+ return list(
+ "scanmode" = scan_mode,
+ "silent" = silent_mode,
+ "fibers" = fibers,
+ "fingerprints" = fingerprints,
+ "chosen_fiber" = chosen_fiber,
+ "chosen_fingerprint" = chosen_fingerprint,
+ )
+
+/obj/item/forensics_spoofer/ui_act(action, list/params, datum/tgui/ui, datum/ui_state/state)
+ . = ..()
+ if(.)
+ return
+ if(!isnull(params["chosen"])) //fiber/print actions
+ var/chosen = params["chosen"]
+ switch(action)
+ if("delete")
+ if(chosen in fibers)
+ if(chosen_fiber == chosen)
+ chosen_fiber = null
+ fibers -= chosen
+ else
+ if(chosen_fingerprint == chosen)
+ chosen_fingerprint = null
+ fingerprints -= chosen
+ return TRUE
+ if("choose")
+ var/is_fiber = !!(chosen in fibers)
+ chosen_fiber = is_fiber ? chosen : null
+ chosen_fingerprint = is_fiber ? null : chosen
+ return TRUE
+ if("make_note")
+ if(chosen in fibers)
+ fibers[chosen] = params["note"]
+ else
+ fingerprints[chosen] = params["note"]
+ return TRUE
+ else
+ switch(action)
+ if("scanmode")
+ scan_mode = !scan_mode
+ return TRUE
+ if("stealth")
+ silent_mode = !silent_mode
+ return TRUE
diff --git a/code/game/objects/items/storage/uplink_kits.dm b/code/game/objects/items/storage/uplink_kits.dm
index d945a3fd711db..1d0dd1fc2581d 100644
--- a/code/game/objects/items/storage/uplink_kits.dm
+++ b/code/game/objects/items/storage/uplink_kits.dm
@@ -348,6 +348,22 @@
new /obj/item/gun/ballistic/rifle/rebarxbow/syndie(src)
new /obj/item/storage/bag/rebar_quiver/syndicate(src)
+/obj/item/paper/syndicate_forensics_spoofer
+ name = "Forensics Spoofer Guide"
+ default_raw_text = {"
+ Forensics Spoofer Info:
+ The spoofer has two modes: SCAN which scans for fingerprints and fibers, and APPLY which applies the currently chosen fingerprint/fiber to your target.
+ The spoofer can only store 5 fingerprints and 5 fibers, and may not store or report fibers/prints already stored. Additionally, it taps into the stations network to associate scanned fingerprints with names.
+ The spoofer will make the same sounds and sights as a forensics scanner, when silent mode is off.
+ "}
+
+/obj/item/storage/box/syndie_kit/forensics_spoofer
+ name = "forensics spoofing kit"
+
+/obj/item/storage/box/syndie_kit/forensics_spoofer/PopulateContents()
+ new /obj/item/forensics_spoofer(src)
+ new /obj/item/paper/syndicate_forensics_spoofer(src)
+
/obj/item/storage/box/syndie_kit/origami_bundle
name = "origami kit"
desc = "A box full of a number of rather masterfully engineered paper planes and a manual on \"The Art of Origami\"."
diff --git a/code/game/turfs/baseturfs.dm b/code/game/turfs/baseturfs.dm
index ad016b6344775..b65d1a6b71db4 100644
--- a/code/game/turfs/baseturfs.dm
+++ b/code/game/turfs/baseturfs.dm
@@ -112,17 +112,16 @@
/// Replaces all instances of needle_type in baseturfs with replacement_type
/turf/proc/replace_baseturf(needle_type, replacement_type)
if (islist(baseturfs))
- var/list/new_baseturfs
+ var/list/new_baseturfs = baseturfs.Copy()
- while (TRUE)
- var/found_index = baseturfs.Find(needle_type)
+ for(var/base_i in 1 to length(new_baseturfs))
+ var/found_index = new_baseturfs.Find(needle_type)
if (found_index == 0)
break
- new_baseturfs ||= baseturfs.Copy()
new_baseturfs[found_index] = replacement_type
- if (!isnull(new_baseturfs))
+ if (length(new_baseturfs))
baseturfs = baseturfs_string_list(new_baseturfs, src)
else if (baseturfs == needle_type)
baseturfs = replacement_type
diff --git a/code/modules/client/client_procs.dm b/code/modules/client/client_procs.dm
index 062a402cc3f23..36ca38124a271 100644
--- a/code/modules/client/client_procs.dm
+++ b/code/modules/client/client_procs.dm
@@ -249,6 +249,9 @@ GLOBAL_LIST_INIT(blacklisted_builds, list(
GLOB.clients += src
GLOB.directory[ckey] = src
+ if(byond_version >= 516)
+ winset(src, null, list("browser-options" = "find,refresh,byondstorage"))
+
// Instantiate stat panel
stat_panel = new(src, "statbrowser")
stat_panel.subscribe(src, PROC_REF(on_stat_panel_message))
diff --git a/code/modules/holiday/holidays.dm b/code/modules/holiday/holidays.dm
index d5a9457141294..3138fff9032c1 100644
--- a/code/modules/holiday/holidays.dm
+++ b/code/modules/holiday/holidays.dm
@@ -705,7 +705,7 @@
/datum/holiday/xmas
name = CHRISTMAS
- begin_day = 23
+ begin_day = 18
begin_month = DECEMBER
end_day = 27
holiday_hat = /obj/item/clothing/head/costume/santa
diff --git a/code/modules/holodeck/computer.dm b/code/modules/holodeck/computer.dm
index ace4fc62aa6f0..2257f4c079651 100644
--- a/code/modules/holodeck/computer.dm
+++ b/code/modules/holodeck/computer.dm
@@ -365,6 +365,8 @@ GLOBAL_LIST_INIT(typecache_holodeck_linked_floorcheck_ok, typecacheof(list(/turf
if(SPT_PROB(2.5, seconds_per_tick))
do_sparks(2, 1, holo_turf)
return
+ if(spawning_simulation)
+ return // putting it here because updating power would be pointless we are only loading it
. = ..()
if(!. || program == offline_program)//we dont need to scan the holodeck if the holodeck is offline
update_use_power(IDLE_POWER_USE)
diff --git a/code/modules/holodeck/holodeck_map_templates.dm b/code/modules/holodeck/holodeck_map_templates.dm
index e7354ceb70f4c..445574c7e03c9 100644
--- a/code/modules/holodeck/holodeck_map_templates.dm
+++ b/code/modules/holodeck/holodeck_map_templates.dm
@@ -1,134 +1,112 @@
/datum/map_template/holodeck
+ /// id
var/template_id
- var/description
+ /// Is this an emag program
var/restricted = FALSE
- var/datum/parsed_map/lastparsed
should_place_on_top = FALSE
returns_created_atoms = TRUE
keep_cached_map = TRUE
- var/obj/machinery/computer/holodeck/linked
-
/datum/map_template/holodeck/offline
name = "Holodeck - Offline"
template_id = "holodeck_offline"
- description = "benis"
mappath = "_maps/templates/holodeck_offline.dmm"
/datum/map_template/holodeck/emptycourt
name = "Holodeck - Empty Court"
template_id = "holodeck_emptycourt"
- description = "benis"
mappath = "_maps/templates/holodeck_emptycourt.dmm"
/datum/map_template/holodeck/dodgeball
name = "Holodeck - Dodgeball Court"
template_id = "holodeck_dodgeball"
- description = "benis"
mappath = "_maps/templates/holodeck_dodgeball.dmm"
/datum/map_template/holodeck/basketball
name = "Holodeck - Basketball Court"
template_id = "holodeck_basketball"
- description = "benis"
mappath = "_maps/templates/holodeck_basketball.dmm"
/datum/map_template/holodeck/thunderdome
name = "Holodeck - Thunderdome Arena"
template_id = "holodeck_thunderdome"
- description = "benis"
mappath = "_maps/templates/holodeck_thunderdome.dmm"
/datum/map_template/holodeck/beach
name = "Holodeck - Beach"
template_id = "holodeck_beach"
- description = "benis"
mappath = "_maps/templates/holodeck_beach.dmm"
/datum/map_template/holodeck/lounge
name = "Holodeck - Lounge"
template_id = "holodeck_lounge"
- description = "benis"
mappath = "_maps/templates/holodeck_lounge.dmm"
/datum/map_template/holodeck/petpark
name = "Holodeck - Pet Park"
template_id = "holodeck_petpark"
- description = "benis"
mappath = "_maps/templates/holodeck_petpark.dmm"
/datum/map_template/holodeck/firingrange
name = "Holodeck - Firing Range"
template_id = "holodeck_firingrange"
- description = "benis"
mappath = "_maps/templates/holodeck_firingrange.dmm"
/datum/map_template/holodeck/anime_school
name = "Holodeck - Anime School"
template_id = "holodeck_animeschool"
- description = "benis"
mappath = "_maps/templates/holodeck_animeschool.dmm"
/datum/map_template/holodeck/chapelcourt
name = "Holodeck - Chapel Courtroom"
template_id = "holodeck_chapelcourt"
- description = "benis"
mappath = "_maps/templates/holodeck_chapelcourt.dmm"
/datum/map_template/holodeck/spacechess
name = "Holodeck - Space Chess"
template_id = "holodeck_spacechess"
- description = "benis"
mappath = "_maps/templates/holodeck_spacechess.dmm"
/datum/map_template/holodeck/spacecheckers
name = "Holodeck - Space Checkers"
template_id = "holodeck_spacecheckers"
- description = "benis"
mappath = "_maps/templates/holodeck_spacecheckers.dmm"
/datum/map_template/holodeck/kobayashi
name = "Holodeck - Kobayashi Maru"
template_id = "holodeck_kobayashi"
- description = "benis"
mappath = "_maps/templates/holodeck_kobayashi.dmm"
/datum/map_template/holodeck/winterwonderland
name = "Holodeck - Winter Wonderland"
template_id = "holodeck_winterwonderland"
- description = "benis"
mappath = "_maps/templates/holodeck_winterwonderland.dmm"
/datum/map_template/holodeck/photobooth
name = "Holodeck - Photobooth"
template_id = "holodeck_photobooth"
- description = "benis"
mappath = "_maps/templates/holodeck_photobooth.dmm"
/datum/map_template/holodeck/skatepark
name = "Holodeck - Skatepark"
template_id = "holodeck_skatepark"
- description = "benis"
mappath = "_maps/templates/holodeck_skatepark.dmm"
/datum/map_template/holodeck/microwave
name = "Holodeck - Microwave Paradise"
template_id = "holodeck_microwave"
- description = "benis"
mappath = "_maps/templates/holodeck_microwave.dmm"
/datum/map_template/holodeck/baseball
name = "Holodeck - Baseball Field"
template_id = "holodeck_baseball"
- description = "benis"
mappath = "_maps/templates/holodeck_baseball.dmm"
/datum/map_template/holodeck/card_battle
name = "Holodeck - TGC Battle Arena"
template_id = "holodeck_card_battle"
- description = "An arena for playing Tactical Game Cards."
mappath = "_maps/templates/holodeck_card_battle.dmm"
//bad evil no good programs
@@ -136,48 +114,41 @@
/datum/map_template/holodeck/medicalsim
name = "Holodeck - Emergency Medical"
template_id = "holodeck_medicalsim"
- description = "benis"
mappath = "_maps/templates/holodeck_medicalsim.dmm"
restricted = TRUE
/datum/map_template/holodeck/thunderdome1218
name = "Holodeck - 1218 AD"
template_id = "holodeck_thunderdome1218"
- description = "benis"
mappath = "_maps/templates/holodeck_thunderdome1218.dmm"
restricted = TRUE
/datum/map_template/holodeck/burntest
name = "Holodeck - Atmospheric Burn Test"
template_id = "holodeck_burntest"
- description = "benis"
mappath = "_maps/templates/holodeck_burntest.dmm"
restricted = TRUE
/datum/map_template/holodeck/wildlifesim
name = "Holodeck - Wildlife Simulation"
template_id = "holodeck_wildlifesim"
- description = "benis"
mappath = "_maps/templates/holodeck_wildlifesim.dmm"
restricted = TRUE
/datum/map_template/holodeck/holdoutbunker
name = "Holodeck - Holdout Bunker"
template_id = "holodeck_holdoutbunker"
- description = "benis"
mappath = "_maps/templates/holodeck_holdoutbunker.dmm"
restricted = TRUE
/datum/map_template/holodeck/anthophillia
name = "Holodeck - Anthophillia"
template_id = "holodeck_anthophillia"
- description = "benis"
mappath = "_maps/templates/holodeck_anthophillia.dmm"
restricted = TRUE
/datum/map_template/holodeck/refuelingstation
name = "Holodeck - Refueling Station"
template_id = "holodeck_refuelingstation"
- description = "benis"
mappath = "_maps/templates/holodeck_refuelingstation.dmm"
restricted = TRUE
diff --git a/code/modules/jobs/job_types/chaplain/chaplain_nullrod.dm b/code/modules/jobs/job_types/chaplain/chaplain_nullrod.dm
index f23fdb7f76d50..64fe89085f60d 100644
--- a/code/modules/jobs/job_types/chaplain/chaplain_nullrod.dm
+++ b/code/modules/jobs/job_types/chaplain/chaplain_nullrod.dm
@@ -190,10 +190,10 @@
menu_description = "An odd sharp blade which provides a low chance of blocking incoming melee attacks and deals a random amount of damage, which can range from almost nothing to very high. Can be worn on the back."
/obj/item/nullrod/claymore/multiverse/melee_attack_chain(mob/user, atom/target, params)
- var/old_force = force
- force += rand(-14, 15)
+ var/force_mod = rand(-14, 15)
+ force += force_mod
. = ..()
- force = old_force
+ force -= force_mod
/obj/item/nullrod/claymore/saber
name = "light energy sword"
@@ -839,9 +839,11 @@
//We do this because our force could have been changed by things like whetstones and RPG stats.
force += old_force - initial(force)
+ //Record change to our force in case something modifies it down the chain
+ var/force_diff = force - old_force
. = ..()
//Reapply our old force.
- force = old_force
+ force -= force_diff
/obj/item/nullrod/nullblade/afterattack(atom/target, mob/user, click_parameters)
if(!isliving(target))
diff --git a/code/modules/paperwork/clipboard.dm b/code/modules/paperwork/clipboard.dm
index 968a093684b60..435cfc3e7c74a 100644
--- a/code/modules/paperwork/clipboard.dm
+++ b/code/modules/paperwork/clipboard.dm
@@ -92,14 +92,23 @@
/obj/item/clipboard/update_overlays()
. = ..()
- var/obj/item/paper/toppaper = toppaper_ref?.resolve()
- if(toppaper)
- . += toppaper.icon_state
- . += toppaper.overlays
+ var/paper_to_add = get_paper_overlay()
+ if(paper_to_add)
+ . += paper_to_add
if(pen)
. += "clipboard_pen"
. += "clipboard_over"
+/obj/item/clipboard/proc/get_paper_overlay()
+ var/obj/item/paper/toppaper = toppaper_ref?.resolve()
+ if(isnull(toppaper))
+ return
+
+ var/mutable_appearance/paper_overlay = mutable_appearance(icon, toppaper.icon_state, offset_spokesman = src, appearance_flags = KEEP_APART)
+ paper_overlay = toppaper.color_atom_overlay(paper_overlay)
+ paper_overlay.overlays += toppaper.overlays
+ return paper_overlay
+
/obj/item/clipboard/attack_hand(mob/user, list/modifiers)
if(LAZYACCESS(modifiers, RIGHT_CLICK))
var/obj/item/paper/toppaper = toppaper_ref?.resolve()
diff --git a/code/modules/paperwork/paper.dm b/code/modules/paperwork/paper.dm
index d974141bc19c8..7a027eb8c759c 100644
--- a/code/modules/paperwork/paper.dm
+++ b/code/modules/paperwork/paper.dm
@@ -278,9 +278,9 @@
if(LAZYLEN(stamp_cache) > MAX_PAPER_STAMPS_OVERLAYS)
return
- var/mutable_appearance/stamp_overlay = mutable_appearance('icons/obj/service/bureaucracy.dmi', "paper_[stamp_icon_state]")
- stamp_overlay.pixel_x = rand(-2, 2)
- stamp_overlay.pixel_y = rand(-3, 2)
+ var/mutable_appearance/stamp_overlay = mutable_appearance('icons/obj/service/bureaucracy.dmi', "paper_[stamp_icon_state]", appearance_flags = KEEP_APART | RESET_COLOR)
+ stamp_overlay.pixel_w = rand(-2, 2)
+ stamp_overlay.pixel_z = rand(-3, 2)
add_overlay(stamp_overlay)
LAZYADD(stamp_cache, stamp_icon_state)
@@ -306,6 +306,8 @@
/obj/item/paper/update_icon_state()
if(LAZYLEN(raw_text_inputs) && show_written_words)
icon_state = "[initial(icon_state)]_words"
+ else
+ icon_state = initial(icon_state)
return ..()
/obj/item/paper/verb/rename()
diff --git a/code/modules/reagents/chemistry/reagents/other_reagents.dm b/code/modules/reagents/chemistry/reagents/other_reagents.dm
index 0c2e365401365..16e992e2efc2a 100644
--- a/code/modules/reagents/chemistry/reagents/other_reagents.dm
+++ b/code/modules/reagents/chemistry/reagents/other_reagents.dm
@@ -1632,6 +1632,7 @@
description = "A powder that is used for coloring things."
color = COLOR_WHITE
taste_description = "the back of class"
+ can_color_organs = TRUE
var/colorname = "none"
/datum/reagent/colorful_reagent/powder/New()
@@ -1707,51 +1708,51 @@
name = "White Powder"
colorname = "white"
color = COLOR_WHITE
- random_color_list = list(COLOR_WHITE) //doesn't actually change appearance at all
+ random_color_list = list(COLOR_WHITE)
chemical_flags = REAGENT_CAN_BE_SYNTHESIZED
/* used by crayons, can't color living things but still used for stuff like food recipes */
/datum/reagent/colorful_reagent/powder/red/crayon
name = "Red Crayon Powder"
- can_colour_mobs = FALSE
+ can_color_mobs = FALSE
chemical_flags = REAGENT_CAN_BE_SYNTHESIZED
/datum/reagent/colorful_reagent/powder/orange/crayon
name = "Orange Crayon Powder"
- can_colour_mobs = FALSE
+ can_color_mobs = FALSE
chemical_flags = REAGENT_CAN_BE_SYNTHESIZED
/datum/reagent/colorful_reagent/powder/yellow/crayon
name = "Yellow Crayon Powder"
- can_colour_mobs = FALSE
+ can_color_mobs = FALSE
chemical_flags = REAGENT_CAN_BE_SYNTHESIZED
/datum/reagent/colorful_reagent/powder/green/crayon
name = "Green Crayon Powder"
- can_colour_mobs = FALSE
+ can_color_mobs = FALSE
chemical_flags = REAGENT_CAN_BE_SYNTHESIZED
/datum/reagent/colorful_reagent/powder/blue/crayon
name = "Blue Crayon Powder"
- can_colour_mobs = FALSE
+ can_color_mobs = FALSE
chemical_flags = REAGENT_CAN_BE_SYNTHESIZED
/datum/reagent/colorful_reagent/powder/purple/crayon
name = "Purple Crayon Powder"
- can_colour_mobs = FALSE
+ can_color_mobs = FALSE
chemical_flags = REAGENT_CAN_BE_SYNTHESIZED
//datum/reagent/colorful_reagent/powder/invisible/crayon
/datum/reagent/colorful_reagent/powder/black/crayon
name = "Black Crayon Powder"
- can_colour_mobs = FALSE
+ can_color_mobs = FALSE
chemical_flags = REAGENT_CAN_BE_SYNTHESIZED
/datum/reagent/colorful_reagent/powder/white/crayon
name = "White Crayon Powder"
- can_colour_mobs = FALSE
+ can_color_mobs = FALSE
chemical_flags = REAGENT_CAN_BE_SYNTHESIZED
//////////////////////////////////Hydroponics stuff///////////////////////////////
@@ -2163,8 +2164,13 @@
var/list/random_color_list = list("#00aedb","#a200ff","#f47835","#d41243","#d11141","#00b159","#00aedb","#f37735","#ffc425","#008744","#0057e7","#d62d20","#ffa700")
color = COLOR_GRAY
taste_description = "rainbows"
- var/can_colour_mobs = TRUE
chemical_flags = REAGENT_CAN_BE_SYNTHESIZED
+ /// Whenever this reagent can color mob limbs and organs upon exposure
+ var/can_color_mobs = TRUE
+ /// Whenever this reagent can color mob equipment when they're exposed to it externally
+ var/can_color_clothing = TRUE
+ /// Whenever this reagent can color mob organs when taken internally
+ var/can_color_organs = FALSE // False by default as this would cause chaotic flickering of victim's eyes
var/datum/callback/color_callback
/datum/reagent/colorful_reagent/New()
@@ -2181,15 +2187,63 @@
color_callback = null
color = pick(random_color_list)
+/datum/reagent/colorful_reagent/expose_mob(mob/living/exposed_mob, methods, reac_volume, show_message, touch_protection)
+ . = ..()
+ var/picked_color = pick(random_color_list)
+ var/color_filter = color_transition_filter(picked_color, SATURATION_OVERRIDE)
+ if (can_color_clothing && (methods & TOUCH|VAPOR|INHALE))
+ var/include_flags = INCLUDE_HELD|INCLUDE_ACCESSORIES
+ if (methods & VAPOR|INHALE)
+ include_flags |= INCLUDE_POCKETS
+ // Not as anyting because this can produce nulls with the flags we passed
+ for (var/obj/item/to_color in exposed_mob.get_equipped_items(include_flags))
+ to_color.add_atom_colour(color_filter, WASHABLE_COLOUR_PRIORITY)
+
+ if (ishuman(exposed_mob))
+ var/mob/living/carbon/human/exposed_human = exposed_mob
+ exposed_human.set_facial_haircolor(picked_color, update = FALSE)
+ exposed_human.set_haircolor(picked_color)
+
+ if (!can_color_mobs)
+ return
+
+ if (!iscarbon(exposed_mob))
+ exposed_mob.add_atom_colour(color_filter, WASHABLE_COLOUR_PRIORITY)
+ return
+
+ if (!(methods & TOUCH|VAPOR|INHALE))
+ return
+
+ var/mob/living/carbon/exposed_carbon = exposed_mob
+ for (var/obj/item/bodypart/part as anything in exposed_carbon.bodyparts)
+ part.add_atom_colour(color_filter, WASHABLE_COLOUR_PRIORITY)
+
+ for (var/obj/item/organ/organ as anything in exposed_carbon.organs)
+ organ.add_atom_colour(color_filter, WASHABLE_COLOUR_PRIORITY)
+
/datum/reagent/colorful_reagent/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, times_fired)
. = ..()
- if(can_colour_mobs)
- affected_mob.add_atom_colour(color_transition_filter(pick(random_color_list), SATURATION_OVERRIDE), WASHABLE_COLOUR_PRIORITY)
+
+ if (!iscarbon(affected_mob))
+ if (can_color_mobs)
+ affected_mob.add_atom_colour(color_transition_filter(pick(random_color_list), SATURATION_OVERRIDE), WASHABLE_COLOUR_PRIORITY)
+ return
+
+ if(!can_color_organs)
+ return
+
+ var/mob/living/carbon/carbon_mob = affected_mob
+ var/color_priority = WASHABLE_COLOUR_PRIORITY
+ if (current_cycle >= 30) // Seeps deep into your tissues
+ color_priority = FIXED_COLOUR_PRIORITY
+
+ for (var/obj/item/organ/organ as anything in carbon_mob.organs)
+ organ.add_atom_colour(color_transition_filter(pick(random_color_list), SATURATION_OVERRIDE), color_priority)
/// Colors anything it touches a random color.
/datum/reagent/colorful_reagent/expose_atom(atom/exposed_atom, reac_volume)
. = ..()
- if(!isliving(exposed_atom) || can_colour_mobs)
+ if(!isliving(exposed_atom))
exposed_atom.add_atom_colour(color_transition_filter(pick(random_color_list), SATURATION_OVERRIDE), WASHABLE_COLOUR_PRIORITY)
/datum/reagent/hair_dye
@@ -2210,7 +2264,7 @@
/datum/reagent/hair_dye/expose_mob(mob/living/exposed_mob, methods=TOUCH, reac_volume, show_message=TRUE, touch_protection=FALSE)
. = ..()
- if(!(methods & (TOUCH|VAPOR)) || !ishuman(exposed_mob))
+ if(!(methods & (TOUCH|VAPOR|INHALE)) || !ishuman(exposed_mob))
return
var/mob/living/carbon/human/exposed_human = exposed_mob
diff --git a/code/modules/recycling/conveyor.dm b/code/modules/recycling/conveyor.dm
index 44d9631a60950..9e03dc98aeacc 100644
--- a/code/modules/recycling/conveyor.dm
+++ b/code/modules/recycling/conveyor.dm
@@ -248,8 +248,10 @@ GLOBAL_LIST_EMPTY(conveyors_by_id)
start_conveying(movable)
return TRUE
-/obj/machinery/conveyor/proc/conveyable_enter(datum/source, atom/convayable)
+/obj/machinery/conveyor/proc/conveyable_enter(datum/source, atom/movable/convayable)
SIGNAL_HANDLER
+ if(convayable.loc != loc) // If we are not on the same turf (order of operations memes) go to hell
+ return
if(operating == CONVEYOR_OFF)
GLOB.move_manager.stop_looping(convayable, SSconveyors)
return
diff --git a/code/modules/surgery/bodyparts/_bodyparts.dm b/code/modules/surgery/bodyparts/_bodyparts.dm
index 1c0718b98e90f..2588882d2ada9 100644
--- a/code/modules/surgery/bodyparts/_bodyparts.dm
+++ b/code/modules/surgery/bodyparts/_bodyparts.dm
@@ -786,7 +786,7 @@
SIGNAL_ADDTRAIT(TRAIT_NOBLOOD),
))
- UnregisterSignal(old_owner, COMSIG_ATOM_RESTYLE)
+ UnregisterSignal(old_owner, list(COMSIG_ATOM_RESTYLE, COMSIG_COMPONENT_CLEAN_ACT))
/// Apply ownership of a limb to someone, giving the appropriate traits, updates and signals
/obj/item/bodypart/proc/apply_ownership(mob/living/carbon/new_owner)
@@ -815,6 +815,7 @@
update_disabled()
RegisterSignal(owner, COMSIG_ATOM_RESTYLE, PROC_REF(on_attempt_feature_restyle_mob))
+ RegisterSignal(owner, COMSIG_COMPONENT_CLEAN_ACT, PROC_REF(on_owner_clean))
forceMove(owner)
RegisterSignal(src, COMSIG_MOVABLE_MOVED, PROC_REF(on_forced_removal)) //this must be set after we moved, or we insta gib
@@ -970,7 +971,12 @@
/obj/item/bodypart/proc/remove_color_override(color_priority)
LAZYREMOVE(color_overrides, "[color_priority]")
-//to update the bodypart's icon when not attached to a mob
+/// Called when limb's current owner gets washed
+/obj/item/bodypart/proc/on_owner_clean(mob/living/carbon/source, clean_types)
+ SIGNAL_HANDLER
+ wash(clean_types)
+
+/// To update the bodypart's icon when not attached to a mob
/obj/item/bodypart/proc/update_icon_dropped()
SHOULD_CALL_PARENT(TRUE)
@@ -984,6 +990,24 @@
img.pixel_y += px_y
add_overlay(standing)
+/obj/item/bodypart/update_atom_colour()
+ . = ..()
+ for(var/i in 1 to COLOUR_PRIORITY_AMOUNT)
+ var/list/checked_color = atom_colours[i]
+ if (!checked_color)
+ remove_color_override(LIMB_COLOR_ATOM_COLOR + i)
+ continue
+ var/actual_color = checked_color[ATOM_COLOR_VALUE_INDEX]
+ if (checked_color[ATOM_COLOR_TYPE_INDEX] == ATOM_COLOR_TYPE_FILTER)
+ var/color_filter = checked_color[ATOM_COLOR_VALUE_INDEX]
+ actual_color = apply_matrix_to_color(COLOR_WHITE, color_filter["color"], color_filter["space"] || COLORSPACE_RGB)
+ add_color_override(actual_color, LIMB_COLOR_ATOM_COLOR + i)
+ update_limb()
+ if (owner)
+ owner.update_body_parts()
+ else
+ update_icon_dropped()
+
///Generates an /image for the limb to be used as an overlay
/obj/item/bodypart/proc/get_limb_icon(dropped)
SHOULD_CALL_PARENT(TRUE)
diff --git a/code/modules/surgery/organs/internal/eyes/_eyes.dm b/code/modules/surgery/organs/internal/eyes/_eyes.dm
index daf0b0b060cd3..7c65a6d68769a 100644
--- a/code/modules/surgery/organs/internal/eyes/_eyes.dm
+++ b/code/modules/surgery/organs/internal/eyes/_eyes.dm
@@ -56,6 +56,7 @@
apply_damaged_eye_effects()
refresh(receiver, call_update = !special)
RegisterSignal(receiver, COMSIG_ATOM_BULLET_ACT, PROC_REF(on_bullet_act))
+ RegisterSignal(receiver, COMSIG_COMPONENT_CLEAN_FACE_ACT, PROC_REF(on_face_wash))
if (scarring)
apply_scarring_effects()
@@ -69,10 +70,11 @@
return
var/mob/living/carbon/human/affected_human = eye_owner
- if(eye_color_left)
+ if(length(eye_color_left))
affected_human.add_eye_color_left(eye_color_left, EYE_COLOR_ORGAN_PRIORITY, update_body = FALSE)
- if(eye_color_right)
+ if(length(eye_color_right))
affected_human.add_eye_color_right(eye_color_right, EYE_COLOR_ORGAN_PRIORITY, update_body = FALSE)
+ refresh_atom_color_overrides()
if(HAS_TRAIT(affected_human, TRAIT_NIGHT_VISION) && !lighting_cutoff)
lighting_cutoff = LIGHTING_CUTOFF_REAL_LOW
@@ -88,6 +90,8 @@
if(ishuman(organ_owner))
var/mob/living/carbon/human/human_owner = organ_owner
human_owner.remove_eye_color(EYE_COLOR_ORGAN_PRIORITY, update_body = FALSE)
+ for(var/i in 1 to COLOUR_PRIORITY_AMOUNT)
+ human_owner.remove_eye_color(EYE_COLOR_ATOM_COLOR_PRIORITY + i, update_body = FALSE)
if(native_fov)
organ_owner.remove_fov_trait(type)
if(!special)
@@ -105,7 +109,45 @@
organ_owner.update_tint()
organ_owner.update_sight()
- UnregisterSignal(organ_owner, COMSIG_ATOM_BULLET_ACT)
+ UnregisterSignal(organ_owner, list(COMSIG_ATOM_BULLET_ACT, COMSIG_COMPONENT_CLEAN_FACE_ACT))
+
+/obj/item/organ/eyes/update_atom_colour()
+ . = ..()
+ if (ishuman(owner))
+ refresh_atom_color_overrides()
+ owner.update_body()
+
+/// Adds eye color overrides to our owner from our atom color
+/obj/item/organ/eyes/proc/refresh_atom_color_overrides()
+ if (!atom_colours)
+ return
+
+ var/mob/living/carbon/human/human_owner = owner
+ for(var/i in 1 to COLOUR_PRIORITY_AMOUNT)
+ var/list/checked_color = atom_colours[i]
+ if (!checked_color)
+ human_owner.remove_eye_color(EYE_COLOR_ATOM_COLOR_PRIORITY + i, update_body = FALSE)
+ continue
+
+ var/left_color = COLOR_WHITE
+ var/right_color = COLOR_WHITE
+
+ if (length(eye_color_left))
+ left_color = eye_color_left
+ if (length(eye_color_right))
+ right_color = eye_color_right
+
+ if (checked_color[ATOM_COLOR_TYPE_INDEX] == ATOM_COLOR_TYPE_FILTER)
+ var/color_filter = checked_color[ATOM_COLOR_VALUE_INDEX]
+ left_color = apply_matrix_to_color(left_color, color_filter["color"], color_filter["space"] || COLORSPACE_RGB)
+ right_color = apply_matrix_to_color(right_color, color_filter["color"], color_filter["space"] || COLORSPACE_RGB)
+ else
+ var/list/target_color = color_transition_filter(checked_color[ATOM_COLOR_VALUE_INDEX], SATURATION_OVERRIDE)
+ left_color = apply_matrix_to_color(left_color, target_color["color"], COLORSPACE_HSL)
+ right_color = apply_matrix_to_color(right_color, target_color["color"], COLORSPACE_HSL)
+
+ human_owner.add_eye_color_left(left_color, EYE_COLOR_ATOM_COLOR_PRIORITY + i, update_body = FALSE)
+ human_owner.add_eye_color_right(right_color, EYE_COLOR_ATOM_COLOR_PRIORITY + i, update_body = FALSE)
/obj/item/organ/eyes/proc/on_bullet_act(mob/living/carbon/source, obj/projectile/proj, def_zone, piercing_hit, blocked)
SIGNAL_HANDLER
@@ -134,6 +176,11 @@
eye_puncture.apply_wound(bodypart_owner, wound_source = "bullet impact", right_side = picked_side)
apply_scar(picked_side)
+/// When our owner washes their face. The idea that spessmen wash their eyeballs is highly disturbing but this is the easiest way to get rid of cursed crayon eye coloring
+/obj/item/organ/eyes/proc/on_face_wash()
+ SIGNAL_HANDLER
+ wash(CLEAN_WASH)
+
#define OFFSET_X 1
#define OFFSET_Y 2
diff --git a/code/modules/uplink/uplink_items/stealthy_tools.dm b/code/modules/uplink/uplink_items/stealthy_tools.dm
index 000364f27be47..7268ef5efe359 100644
--- a/code/modules/uplink/uplink_items/stealthy_tools.dm
+++ b/code/modules/uplink/uplink_items/stealthy_tools.dm
@@ -104,6 +104,13 @@
cost = 1
surplus = 30
+/datum/uplink_item/stealthy_tools/forensics_spofer
+ name = "Forensics Spoofing Kit"
+ desc = "A box that contains the forensics spoofer (and instructions) which can scan and replicate fingerprints and fibers \
+ and apply them to a target object. Helpful for framing crew. Recommend buying soap with your purchase."
+ item = /obj/item/storage/box/syndie_kit/forensics_spoofer
+ cost = 5
+
/datum/uplink_item/stealthy_tools/telecomm_blackout
name = "Disable Telecomms"
desc = "When purchased, a virus will be uploaded to the telecommunication processing servers to temporarily disable themselves."
diff --git a/html/changelogs/AutoChangeLog-pr-88770.yml b/html/changelogs/AutoChangeLog-pr-88770.yml
new file mode 100644
index 0000000000000..546859be1b78c
--- /dev/null
+++ b/html/changelogs/AutoChangeLog-pr-88770.yml
@@ -0,0 +1,4 @@
+author: "Ghommie"
+delete-after: True
+changes:
+ - bugfix: "Examining a trophy fish no longer shows the current day instead of when it was actually caught and put on the mount."
\ No newline at end of file
diff --git a/html/changelogs/archive/2024-12.yml b/html/changelogs/archive/2024-12.yml
index 0a1a2e5323812..1d9a421178bad 100644
--- a/html/changelogs/archive/2024-12.yml
+++ b/html/changelogs/archive/2024-12.yml
@@ -778,3 +778,10 @@
oddities!
mc-oofert:
- qol: you can adjust diagonal walls to be not diagonal walls with a wrench
+2024-12-31:
+ Kylerace:
+ - bugfix: holodeck is slightly less likely to explode the server
+ SmArtKar:
+ - bugfix: Blood no longer gets colored with the item its attached to
+ mc-oofert:
+ - rscadd: forensics spoofing kit for traitors/whoever with an uplink
diff --git a/html/changelogs/archive/2025-01.yml b/html/changelogs/archive/2025-01.yml
new file mode 100644
index 0000000000000..a8c8357d1f2a5
--- /dev/null
+++ b/html/changelogs/archive/2025-01.yml
@@ -0,0 +1,31 @@
+2025-01-01:
+ 00-Steven:
+ - bugfix: Stamps no longer render below the paper sometimes.
+ - bugfix: Stamps no longer inherit the color of the paper they're on.
+ - bugfix: Clearing paper, like by splashing it with ethanol, actually resets its
+ icon state to the cleared version.
+ 00-Steven, SmArtKar:
+ - bugfix: Paper on clipboards uses its own colour rather than that of the clipboard.
+ Absolucy, Flleeppyy:
+ - sound: Added a new, unique sound for polling!
+ Absolucy, S34NW:
+ - bugfix: Chat settings properly save on BYOND 516 now. Settings still won't carry
+ over from 515 tho, 515 and 516 settings will be separate.
+ Arturlang:
+ - bugfix: The unique AI station trait will no longer be able to choose lawsets set
+ as default in the config.
+ LemonInTheDark:
+ - bugfix: Boulders will no longer randomly run free from smelting pipelines! We
+ have enslaved them once more.
+ Namelessfairy and SmArtKar:
+ - bugfix: The Extradimensional Blade no longer infinitely scales damage
+ - bugfix: The nullblade correctly does increased damage when sharpened
+ SmArtKar:
+ - rscadd: 'Changed how colorful reagent and crayon powder work: douse your victims
+ to color their clothing, bodyparts and even internal organs!'
+ - rscadd: You can wash your eyes when washing your face at a sink
+ - bugfix: You can color robotic limbs with left click (again)
+ - bugfix: Mice no longer can spawn in unsafe atmos from garbage spawners
+ mc-oofert:
+ - bugfix: holodeck no longer explodes if the server lags while its loading a new
+ sim
diff --git a/sound/misc/license.txt b/sound/misc/license.txt
index 2e596a4e128e3..945c48e279d1c 100644
--- a/sound/misc/license.txt
+++ b/sound/misc/license.txt
@@ -1,2 +1,4 @@
bloop.ogg by my man Tim Khan
(https://freesound.org/people/tim.kahn/sounds/130377/)
+
+prompt.ogg by Flleeppyy (https://github.com/Flleeppyy), originally for https://github.com/Monkestation/Monkestation2.0/pull/2621
diff --git a/sound/misc/prompt.ogg b/sound/misc/prompt.ogg
new file mode 100644
index 0000000000000..e32942e0d3cb7
Binary files /dev/null and b/sound/misc/prompt.ogg differ
diff --git a/tgstation.dme b/tgstation.dme
index cee6c9606c716..fdafeac5f1af9 100644
--- a/tgstation.dme
+++ b/tgstation.dme
@@ -2403,6 +2403,7 @@
#include "code\game\objects\items\fireaxe.dm"
#include "code\game\objects\items\flamethrower.dm"
#include "code\game\objects\items\flatpacks.dm"
+#include "code\game\objects\items\forensicsspoofer.dm"
#include "code\game\objects\items\frog_statue.dm"
#include "code\game\objects\items\gift.dm"
#include "code\game\objects\items\gun_maintenance.dm"
diff --git a/tgui/global.d.ts b/tgui/global.d.ts
index d0bfdecf8909f..35c0e9f57da10 100644
--- a/tgui/global.d.ts
+++ b/tgui/global.d.ts
@@ -41,6 +41,21 @@ type ByondType = {
*/
windowId: string;
+ /**
+ * True if javascript is running in BYOND.
+ */
+ IS_BYOND: boolean;
+
+ /**
+ * Version of Trident engine of Internet Explorer. Null if N/A.
+ */
+ TRIDENT: number | null;
+
+ /**
+ * Version of Blink engine of WebView2. Null if N/A.
+ */
+ BLINK: number | null;
+
/**
* If `true`, unhandled errors and common mistakes result in a blue screen
* of death, which stops this window from handling incoming messages and
@@ -175,4 +190,13 @@ interface Window {
Byond: ByondType;
__store__: Store;
__augmentStack__: (store: Store) => StackAugmentor;
+
+ // IE IndexedDB stuff.
+ msIndexedDB: IDBFactory;
+ msIDBTransaction: IDBTransaction;
+
+ // 516 byondstorage API.
+ hubStorage: Storage;
+ domainStorage: Storage;
+ serverStorage: Storage;
}
diff --git a/tgui/packages/common/storage.js b/tgui/packages/common/storage.ts
similarity index 53%
rename from tgui/packages/common/storage.js
rename to tgui/packages/common/storage.ts
index acf842f64083b..b2564acf36dc3 100644
--- a/tgui/packages/common/storage.js
+++ b/tgui/packages/common/storage.ts
@@ -7,9 +7,14 @@
*/
export const IMPL_MEMORY = 0;
-export const IMPL_LOCAL_STORAGE = 1;
+export const IMPL_HUB_STORAGE = 1;
export const IMPL_INDEXED_DB = 2;
+type StorageImplementation =
+ | typeof IMPL_MEMORY
+ | typeof IMPL_HUB_STORAGE
+ | typeof IMPL_INDEXED_DB;
+
const INDEXED_DB_VERSION = 1;
const INDEXED_DB_NAME = 'tgui';
const INDEXED_DB_STORE_NAME = 'storage-v1';
@@ -17,7 +22,15 @@ const INDEXED_DB_STORE_NAME = 'storage-v1';
const READ_ONLY = 'readonly';
const READ_WRITE = 'readwrite';
-const testGeneric = (testFn) => () => {
+type StorageBackend = {
+ impl: StorageImplementation;
+ get(key: string): Promise;
+ set(key: string, value: any): Promise;
+ remove(key: string): Promise;
+ clear(): Promise;
+};
+
+const testGeneric = (testFn: () => boolean) => (): boolean => {
try {
return Boolean(testFn());
} catch {
@@ -25,72 +38,77 @@ const testGeneric = (testFn) => () => {
}
};
-// Localstorage can sometimes throw an error, even if DOM storage is not
-// disabled in IE11 settings.
-// See: https://superuser.com/questions/1080011
-// prettier-ignore
-const testLocalStorage = testGeneric(() => (
- window.localStorage && window.localStorage.getItem
-));
+const testHubStorage = testGeneric(
+ () => window.hubStorage && !!window.hubStorage.getItem,
+);
+// TODO: Remove with 516
// prettier-ignore
const testIndexedDb = testGeneric(() => (
(window.indexedDB || window.msIndexedDB)
- && (window.IDBTransaction || window.msIDBTransaction)
+ && !!(window.IDBTransaction || window.msIDBTransaction)
));
-class MemoryBackend {
+class MemoryBackend implements StorageBackend {
+ private store: Record;
+ public impl: StorageImplementation;
+
constructor() {
this.impl = IMPL_MEMORY;
this.store = {};
}
- get(key) {
+ async get(key: string): Promise {
return this.store[key];
}
- set(key, value) {
+ async set(key: string, value: any): Promise {
this.store[key] = value;
}
- remove(key) {
+ async remove(key: string): Promise {
this.store[key] = undefined;
}
- clear() {
+ async clear(): Promise {
this.store = {};
}
}
-class LocalStorageBackend {
+class HubStorageBackend implements StorageBackend {
+ public impl: StorageImplementation;
+
constructor() {
- this.impl = IMPL_LOCAL_STORAGE;
+ this.impl = IMPL_HUB_STORAGE;
}
- get(key) {
- const value = localStorage.getItem(key);
+ async get(key: string): Promise {
+ const value = await window.hubStorage.getItem(key);
if (typeof value === 'string') {
return JSON.parse(value);
}
+ return undefined;
}
- set(key, value) {
- localStorage.setItem(key, JSON.stringify(value));
+ async set(key: string, value: any): Promise {
+ window.hubStorage.setItem(key, JSON.stringify(value));
}
- remove(key) {
- localStorage.removeItem(key);
+ async remove(key: string): Promise {
+ window.hubStorage.removeItem(key);
}
- clear() {
- localStorage.clear();
+ async clear(): Promise {
+ window.hubStorage.clear();
}
}
-class IndexedDbBackend {
+class IndexedDbBackend implements StorageBackend {
+ public impl: StorageImplementation;
+ public dbPromise: Promise;
+
constructor() {
this.impl = IMPL_INDEXED_DB;
- /** @type {Promise} */
this.dbPromise = new Promise((resolve, reject) => {
const indexedDB = window.indexedDB || window.msIndexedDB;
const req = indexedDB.open(INDEXED_DB_NAME, INDEXED_DB_VERSION);
@@ -98,7 +116,12 @@ class IndexedDbBackend {
try {
req.result.createObjectStore(INDEXED_DB_STORE_NAME);
} catch (err) {
- reject(new Error('Failed to upgrade IDB: ' + req.error));
+ reject(
+ new Error(
+ 'Failed to upgrade IDB: ' +
+ (err instanceof Error ? err.message : String(err)),
+ ),
+ );
}
};
req.onsuccess = () => resolve(req.result);
@@ -108,14 +131,14 @@ class IndexedDbBackend {
});
}
- getStore(mode) {
- // prettier-ignore
- return this.dbPromise.then((db) => db
+ private async getStore(mode: IDBTransactionMode): Promise {
+ const db = await this.dbPromise;
+ return db
.transaction(INDEXED_DB_STORE_NAME, mode)
- .objectStore(INDEXED_DB_STORE_NAME));
+ .objectStore(INDEXED_DB_STORE_NAME);
}
- async get(key) {
+ async get(key: string): Promise {
const store = await this.getStore(READ_ONLY);
return new Promise((resolve, reject) => {
const req = store.get(key);
@@ -124,26 +147,19 @@ class IndexedDbBackend {
});
}
- async set(key, value) {
- // The reason we don't _save_ null is because IE 10 does
- // not support saving the `null` type in IndexedDB. How
- // ironic, given the bug below!
- // See: https://github.com/mozilla/localForage/issues/161
- if (value === null) {
- value = undefined;
- }
+ async set(key: string, value: any): Promise {
// NOTE: We deliberately make this operation transactionless
const store = await this.getStore(READ_WRITE);
store.put(value, key);
}
- async remove(key) {
+ async remove(key: string): Promise {
// NOTE: We deliberately make this operation transactionless
const store = await this.getStore(READ_WRITE);
store.delete(key);
}
- async clear() {
+ async clear(): Promise {
// NOTE: We deliberately make this operation transactionless
const store = await this.getStore(READ_WRITE);
store.clear();
@@ -154,9 +170,16 @@ class IndexedDbBackend {
* Web Storage Proxy object, which selects the best backend available
* depending on the environment.
*/
-class StorageProxy {
+class StorageProxy implements StorageBackend {
+ private backendPromise: Promise;
+ public impl: StorageImplementation = IMPL_MEMORY;
+
constructor() {
this.backendPromise = (async () => {
+ if (!Byond.TRIDENT && testHubStorage()) {
+ return new HubStorageBackend();
+ }
+ // TODO: Remove with 516
if (testIndexedDb()) {
try {
const backend = new IndexedDbBackend();
@@ -164,29 +187,29 @@ class StorageProxy {
return backend;
} catch {}
}
- if (testLocalStorage()) {
- return new LocalStorageBackend();
- }
+ console.warn(
+ 'No supported storage backend found. Using in-memory storage.',
+ );
return new MemoryBackend();
})();
}
- async get(key) {
+ async get(key: string): Promise {
const backend = await this.backendPromise;
return backend.get(key);
}
- async set(key, value) {
+ async set(key: string, value: any): Promise {
const backend = await this.backendPromise;
return backend.set(key, value);
}
- async remove(key) {
+ async remove(key: string): Promise {
const backend = await this.backendPromise;
return backend.remove(key);
}
- async clear() {
+ async clear(): Promise {
const backend = await this.backendPromise;
return backend.clear();
}
diff --git a/tgui/packages/tgui/interfaces/ForensicsSpoofer.tsx b/tgui/packages/tgui/interfaces/ForensicsSpoofer.tsx
new file mode 100644
index 0000000000000..8d527ec72fd1b
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/ForensicsSpoofer.tsx
@@ -0,0 +1,150 @@
+import { BooleanLike } from 'common/react';
+import { useState } from 'react';
+
+import { useBackend } from '../backend';
+import {
+ Box,
+ Button,
+ Divider,
+ Icon,
+ Section,
+ Stack,
+ Tabs,
+} from '../components';
+import { Window } from '../layouts';
+
+type Data = {
+ silent: BooleanLike;
+ scanmode: BooleanLike;
+ fibers: string[];
+ fingerprints: string[];
+ chosen_fiber: string;
+ chosen_fingerprint: string;
+ max_storage: number;
+};
+export const ForensicsSpoofer = (props) => {
+ const { act, data } = useBackend();
+ const {
+ silent,
+ scanmode,
+ fibers,
+ fingerprints,
+ chosen_fiber,
+ chosen_fingerprint,
+ max_storage,
+ } = data;
+ const [currentTab, setTab] = useState(0);
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ setTab(0)}
+ width="50%"
+ >
+ Fingerprints {Object.keys(fingerprints).length}/{max_storage}
+
+ setTab(1)}
+ >
+ Fibers {Object.keys(fibers).length}/{max_storage}
+
+
+
+
+ {Object.keys(currentTab === 0 ? fingerprints : fibers).map(
+ (forensic_data, _) => (
+
+
+
+
+ act('choose', { chosen: forensic_data })
+ }
+ />
+
+
+
+
+
+
+ {currentTab === 0
+ ? forensic_data.substring(0, 25)
+ : forensic_data}
+
+
+ {currentTab === 0 && (
+
+
+ ({fingerprints[forensic_data]})
+
+
+ )}
+
+
+ ),
+ )}
+
+
+
+
+
+
+ );
+};