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]}) + + + )} + + + ), + )} + +
+
+
+
+
+ ); +};