From 8016030f582ac13e627d1d5c6a0761d6170116da Mon Sep 17 00:00:00 2001 From: RosSample Date: Sun, 24 Dec 2023 16:02:14 +0300 Subject: [PATCH 01/44] nukeops min players + tc --- code/__DEFINES/antagonists.dm | 2 +- .../nukeop/equipment/nuclear_challenge.dm | 18 ++++++++++++++++-- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/code/__DEFINES/antagonists.dm b/code/__DEFINES/antagonists.dm index a31b9aee40771..3a08a504ad745 100644 --- a/code/__DEFINES/antagonists.dm +++ b/code/__DEFINES/antagonists.dm @@ -11,7 +11,7 @@ #define NUKE_RESULT_HIJACK_NO_DISK 10 /// Min players requireed for nukes to declare war -#define CHALLENGE_MIN_PLAYERS 50 +#define CHALLENGE_MIN_PLAYERS 25 // MASSMETA EDIT //fugitive end results #define FUGITIVE_RESULT_BADASS_HUNTER 0 diff --git a/code/modules/antagonists/nukeop/equipment/nuclear_challenge.dm b/code/modules/antagonists/nukeop/equipment/nuclear_challenge.dm index 734025e9a370e..b2114a8317039 100644 --- a/code/modules/antagonists/nukeop/equipment/nuclear_challenge.dm +++ b/code/modules/antagonists/nukeop/equipment/nuclear_challenge.dm @@ -1,6 +1,11 @@ #define CHALLENGE_TELECRYSTALS 280 #define CHALLENGE_TIME_LIMIT (5 MINUTES) #define CHALLENGE_SHUTTLE_DELAY (25 MINUTES) // 25 minutes, so the ops have at least 5 minutes before the shuttle is callable. +// MASSMETA EDIT START +#define WAR_TC_MIN 125 +#define WAR_TC_MAX 280 +#define WAR_MAX_PAYOUT 50 +// MASSMETA EDIT END GLOBAL_LIST_EMPTY(jam_on_wardec) @@ -22,7 +27,11 @@ GLOBAL_LIST_EMPTY(jam_on_wardec) return declaring_war = TRUE - var/are_you_sure = tgui_alert(user, "Consult your team carefully before you declare war on [station_name()]. Are you sure you want to alert the enemy crew? You have [DisplayTimeText(CHALLENGE_TIME_LIMIT - world.time - SSticker.round_start_time)] to decide.", "Declare war?", list("Yes", "No")) +// MASSMETA EDIT START + var/are_you_sure = tgui_alert(user, "Consult your team carefully before you declare war on [station_name()]]. Are you sure you want to alert the enemy crew? You will get \ + [round(WAR_TC_MIN + (WAR_TC_MAX - WAR_TC_MIN) * (1 - (WAR_MAX_PAYOUT - min(WAR_MAX_PAYOUT, GLOB.joined_player_list.len)) / (WAR_MAX_PAYOUT - CHALLENGE_MIN_PLAYERS)), 1)] \ + extra telecystals. You have [DisplayTimeText(CHALLENGE_TIME_LIMIT - world.time - SSticker.round_start_time)] to decide", "Declare war?", list("Yes", "No")) +// MASSMETA EDIT END declaring_war = FALSE if(!check_allowed(user)) @@ -114,7 +123,7 @@ GLOBAL_LIST_EMPTY(jam_on_wardec) continue uplinks += uplink - var/tc_to_distribute = CHALLENGE_TELECRYSTALS + var/tc_to_distribute = round(WAR_TC_MIN + (WAR_TC_MAX - WAR_TC_MIN) * (1 - (WAR_MAX_PAYOUT - min(WAR_MAX_PAYOUT, GLOB.joined_player_list.len)) / (WAR_MAX_PAYOUT - CHALLENGE_MIN_PLAYERS)), 1) // MASSMETA EDIT var/tc_per_nukie = round(tc_to_distribute / (length(orphans)+length(uplinks))) for (var/datum/component/uplink/uplink in uplinks) @@ -197,3 +206,8 @@ GLOBAL_LIST_EMPTY(jam_on_wardec) #undef CHALLENGE_TELECRYSTALS #undef CHALLENGE_TIME_LIMIT #undef CHALLENGE_SHUTTLE_DELAY +// MASSMETA EDIT START +#undef WAR_TC_MIN +#undef WAR_TC_MAX +#undef WAR_MAX_PAYOUT +// MASSMETA EDIT END From 448b213fec3ed29dfea42007ecec52082409010f Mon Sep 17 00:00:00 2001 From: Mothblocks <35135081+Mothblocks@users.noreply.github.com> Date: Tue, 26 Dec 2023 10:02:18 -0800 Subject: [PATCH 02/44] Remove eye surgery fluff line (#80565) ## About The Pull Request Removes a side effect in can_start which MUST be pure. This causes the surgery tgui to spam this message. It's pointless because you can't do the surgery to begin with if this returns false, so you only ever see this where you shouldn't. ## Changelog :cl: fix: Fixed the surgery menu spamming chat messages when on the eyes section of a player with no eyes. /:cl: --- code/modules/surgery/eye_surgery.dm | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/code/modules/surgery/eye_surgery.dm b/code/modules/surgery/eye_surgery.dm index 7b6d7844ee6c4..748dab1f4ec85 100644 --- a/code/modules/surgery/eye_surgery.dm +++ b/code/modules/surgery/eye_surgery.dm @@ -22,10 +22,7 @@ /datum/surgery/eye_surgery/can_start(mob/user, mob/living/carbon/target) var/obj/item/organ/internal/eyes/target_eyes = target.get_organ_slot(ORGAN_SLOT_EYES) - if(!target_eyes) - to_chat(user, span_warning("It's hard to do surgery on someone's eyes when [target.p_they()] [target.p_do()]n't have any.")) - return FALSE - return TRUE + return !isnull(target_eyes) /datum/surgery_step/fix_eyes/preop(mob/user, mob/living/carbon/target, target_zone, obj/item/tool, datum/surgery/surgery) display_results( From aa68297f92f88afa9faed0ffd61f93ae9be5834a Mon Sep 17 00:00:00 2001 From: Jeremiah <42397676+jlsnow301@users.noreply.github.com> Date: Tue, 26 Dec 2023 10:02:29 -0800 Subject: [PATCH 03/44] Fixes search in NTOS messenger (#80563) ## About The Pull Request onChange => onInput ## Why It's Good For The Game searches without hitting enter ## Changelog :cl: fix: NTOS Messenger should search as you type now /:cl: --- tgui/packages/tgui/interfaces/NtosMessenger/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tgui/packages/tgui/interfaces/NtosMessenger/index.tsx b/tgui/packages/tgui/interfaces/NtosMessenger/index.tsx index e6a29c5d7b192..9a9f27adf97e9 100644 --- a/tgui/packages/tgui/interfaces/NtosMessenger/index.tsx +++ b/tgui/packages/tgui/interfaces/NtosMessenger/index.tsx @@ -213,7 +213,7 @@ const ContactsScreen = (props: any) => { width="220px" placeholder="Search by name or job..." value={searchUser} - onChange={(_, value) => setSearchUser(value)} + onInput={(_, value) => setSearchUser(value)} /> From fbb099791fd8a3176ab9c4c9b25be09caf70106b Mon Sep 17 00:00:00 2001 From: orange man <61334995+comfyorange@users.noreply.github.com> Date: Wed, 27 Dec 2023 07:02:37 +1300 Subject: [PATCH 04/44] Automatic changelog for PR #80565 [ci skip] --- html/changelogs/AutoChangeLog-pr-80565.yml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 html/changelogs/AutoChangeLog-pr-80565.yml diff --git a/html/changelogs/AutoChangeLog-pr-80565.yml b/html/changelogs/AutoChangeLog-pr-80565.yml new file mode 100644 index 0000000000000..a52639e66e66f --- /dev/null +++ b/html/changelogs/AutoChangeLog-pr-80565.yml @@ -0,0 +1,4 @@ +author: "Mothblocks" +delete-after: True +changes: + - bugfix: "Fixed the surgery menu spamming chat messages when on the eyes section of a player with no eyes." \ No newline at end of file From 464be9f43242a2f094d9c9bc3a215e86a1e3cbc0 Mon Sep 17 00:00:00 2001 From: orange man <61334995+comfyorange@users.noreply.github.com> Date: Wed, 27 Dec 2023 07:02:47 +1300 Subject: [PATCH 05/44] Automatic changelog for PR #80563 [ci skip] --- html/changelogs/AutoChangeLog-pr-80563.yml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 html/changelogs/AutoChangeLog-pr-80563.yml diff --git a/html/changelogs/AutoChangeLog-pr-80563.yml b/html/changelogs/AutoChangeLog-pr-80563.yml new file mode 100644 index 0000000000000..3966b3074d493 --- /dev/null +++ b/html/changelogs/AutoChangeLog-pr-80563.yml @@ -0,0 +1,4 @@ +author: "jlsnow301" +delete-after: True +changes: + - bugfix: "NTOS Messenger should search as you type now" \ No newline at end of file From 5fca80e59e89ea6deb5c79a7db4a03f13e33b15f Mon Sep 17 00:00:00 2001 From: Tim Date: Tue, 26 Dec 2023 13:33:10 -0600 Subject: [PATCH 06/44] Add UI screentips to spraycans (#80564) ## About The Pull Request This adds screentips to spraycans for: - AltClick - toggle cap - RMB - copy - LMB - paint ## Why It's Good For The Game Better UI/UX. ## Changelog :cl: qol: Add UI screentips to spraycans /:cl: --------- Co-authored-by: Mothblocks <35135081+Mothblocks@users.noreply.github.com> --- code/game/objects/items/crayons.dm | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/code/game/objects/items/crayons.dm b/code/game/objects/items/crayons.dm index 5ba709ef324ba..35732e2310173 100644 --- a/code/game/objects/items/crayons.dm +++ b/code/game/objects/items/crayons.dm @@ -762,6 +762,30 @@ /datum/component/slapcrafting,\ slapcraft_recipes = slapcraft_recipe_list,\ ) + register_context() + register_item_context() + +/obj/item/toy/crayon/spraycan/add_context(atom/source, list/context, obj/item/held_item, mob/living/user) + . = ..() + + if(!user.can_perform_action(src, NEED_DEXTERITY|NEED_HANDS)) + return . + + if(has_cap) + context[SCREENTIP_CONTEXT_ALT_LMB] = "Toggle cap" + + return CONTEXTUAL_SCREENTIP_SET + +/obj/item/toy/crayon/spraycan/add_item_context(datum/source, list/context, atom/target, mob/living/user) + . = ..() + + if(!user.can_perform_action(src, NEED_DEXTERITY|NEED_HANDS)) + return . + + context[SCREENTIP_CONTEXT_LMB] = "Paint" + context[SCREENTIP_CONTEXT_RMB] = "Copy color" + + return CONTEXTUAL_SCREENTIP_SET /obj/item/toy/crayon/spraycan/isValidSurface(surface) return (isfloorturf(surface) || iswallturf(surface)) From cc997ffc37724e3e3ed21c868afcc1ee435cafea Mon Sep 17 00:00:00 2001 From: orange man <61334995+comfyorange@users.noreply.github.com> Date: Wed, 27 Dec 2023 08:43:47 +1300 Subject: [PATCH 07/44] Automatic changelog for PR #80564 [ci skip] --- html/changelogs/AutoChangeLog-pr-80564.yml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 html/changelogs/AutoChangeLog-pr-80564.yml diff --git a/html/changelogs/AutoChangeLog-pr-80564.yml b/html/changelogs/AutoChangeLog-pr-80564.yml new file mode 100644 index 0000000000000..39edffca72d7c --- /dev/null +++ b/html/changelogs/AutoChangeLog-pr-80564.yml @@ -0,0 +1,4 @@ +author: "timothymtorres" +delete-after: True +changes: + - qol: "Add UI screentips to spraycans" \ No newline at end of file From 11e5a9ce6b311be8ade303aae062ec9eaf7fb9a6 Mon Sep 17 00:00:00 2001 From: Changelogs Date: Wed, 27 Dec 2023 00:19:39 +0000 Subject: [PATCH 08/44] Automatic changelog compile [ci skip] --- html/changelogs/AutoChangeLog-pr-80561.yml | 4 ---- html/changelogs/AutoChangeLog-pr-80562.yml | 4 ---- html/changelogs/AutoChangeLog-pr-80563.yml | 4 ---- html/changelogs/AutoChangeLog-pr-80564.yml | 4 ---- html/changelogs/AutoChangeLog-pr-80565.yml | 4 ---- html/changelogs/archive/2023-12.yml | 13 +++++++++++++ 6 files changed, 13 insertions(+), 20 deletions(-) delete mode 100644 html/changelogs/AutoChangeLog-pr-80561.yml delete mode 100644 html/changelogs/AutoChangeLog-pr-80562.yml delete mode 100644 html/changelogs/AutoChangeLog-pr-80563.yml delete mode 100644 html/changelogs/AutoChangeLog-pr-80564.yml delete mode 100644 html/changelogs/AutoChangeLog-pr-80565.yml diff --git a/html/changelogs/AutoChangeLog-pr-80561.yml b/html/changelogs/AutoChangeLog-pr-80561.yml deleted file mode 100644 index 379d31e8d41d3..0000000000000 --- a/html/changelogs/AutoChangeLog-pr-80561.yml +++ /dev/null @@ -1,4 +0,0 @@ -author: "Melbert" -delete-after: True -changes: - - bugfix: "Fixed some occasions in which heartbeat SFX will continue on revival for longer than expected" \ No newline at end of file diff --git a/html/changelogs/AutoChangeLog-pr-80562.yml b/html/changelogs/AutoChangeLog-pr-80562.yml deleted file mode 100644 index c366dbea77bfb..0000000000000 --- a/html/changelogs/AutoChangeLog-pr-80562.yml +++ /dev/null @@ -1,4 +0,0 @@ -author: "Ryll/Shaps" -delete-after: True -changes: - - balance: "Yawns are less likely to propagate" \ No newline at end of file diff --git a/html/changelogs/AutoChangeLog-pr-80563.yml b/html/changelogs/AutoChangeLog-pr-80563.yml deleted file mode 100644 index 3966b3074d493..0000000000000 --- a/html/changelogs/AutoChangeLog-pr-80563.yml +++ /dev/null @@ -1,4 +0,0 @@ -author: "jlsnow301" -delete-after: True -changes: - - bugfix: "NTOS Messenger should search as you type now" \ No newline at end of file diff --git a/html/changelogs/AutoChangeLog-pr-80564.yml b/html/changelogs/AutoChangeLog-pr-80564.yml deleted file mode 100644 index 39edffca72d7c..0000000000000 --- a/html/changelogs/AutoChangeLog-pr-80564.yml +++ /dev/null @@ -1,4 +0,0 @@ -author: "timothymtorres" -delete-after: True -changes: - - qol: "Add UI screentips to spraycans" \ No newline at end of file diff --git a/html/changelogs/AutoChangeLog-pr-80565.yml b/html/changelogs/AutoChangeLog-pr-80565.yml deleted file mode 100644 index a52639e66e66f..0000000000000 --- a/html/changelogs/AutoChangeLog-pr-80565.yml +++ /dev/null @@ -1,4 +0,0 @@ -author: "Mothblocks" -delete-after: True -changes: - - bugfix: "Fixed the surgery menu spamming chat messages when on the eyes section of a player with no eyes." \ No newline at end of file diff --git a/html/changelogs/archive/2023-12.yml b/html/changelogs/archive/2023-12.yml index b2cc28dc011f4..d1aa3b3584bc8 100644 --- a/html/changelogs/archive/2023-12.yml +++ b/html/changelogs/archive/2023-12.yml @@ -854,3 +854,16 @@ vinylspiders: - bugfix: fixes a potential mob hard del with cardboard boxes - bugfix: fixes a hard del with thrown items +2023-12-27: + Melbert: + - bugfix: Fixed some occasions in which heartbeat SFX will continue on revival for + longer than expected + Mothblocks: + - bugfix: Fixed the surgery menu spamming chat messages when on the eyes section + of a player with no eyes. + Ryll/Shaps: + - balance: Yawns are less likely to propagate + jlsnow301: + - bugfix: NTOS Messenger should search as you type now + timothymtorres: + - qol: Add UI screentips to spraycans From 13e27c36a64c469a497beb379578c8850aede883 Mon Sep 17 00:00:00 2001 From: Ben10Omintrix <138636438+Ben10Omintrix@users.noreply.github.com> Date: Wed, 27 Dec 2023 13:57:35 +0200 Subject: [PATCH 09/44] basic bot path huds and medbot research (#80277) ## About The Pull Request this pr integrates the bot path huds to ai controllers and move loops to allow basic bots to display their paths in the hud. also closes #80280 and closes #80330 ## Why It's Good For The Game basic bots now can display their path on huds ## Changelog :cl: add: basic bots can now display their paths on huds fix: medbots can research healing again /:cl: --- .../__DEFINES/dcs/signals/signals_moveloop.dm | 2 + .../subsystem/movement/movement_types.dm | 10 ++- code/datums/ai/movement/ai_movement_jps.dm | 15 +++- code/modules/mob/living/basic/bots/_bots.dm | 22 ++++-- code/modules/mob/living/basic/bots/bot_hud.dm | 78 +++++++++++++++++++ .../living/basic/bots/cleanbot/cleanbot.dm | 1 + .../living/basic/bots/cleanbot/cleanbot_ai.dm | 19 +++-- .../mob/living/basic/bots/medbot/medbot.dm | 5 +- .../mob/living/basic/bots/medbot/medbot_ai.dm | 6 +- 9 files changed, 139 insertions(+), 19 deletions(-) diff --git a/code/__DEFINES/dcs/signals/signals_moveloop.dm b/code/__DEFINES/dcs/signals/signals_moveloop.dm index 38ab63a596984..8a354f8bfbb1d 100644 --- a/code/__DEFINES/dcs/signals/signals_moveloop.dm +++ b/code/__DEFINES/dcs/signals/signals_moveloop.dm @@ -9,3 +9,5 @@ #define COMSIG_MOVELOOP_POSTPROCESS "moveloop_postprocess" //from [/datum/move_loop/has_target/jps/recalculate_path] (): #define COMSIG_MOVELOOP_JPS_REPATH "moveloop_jps_repath" +///from [/datum/move_loop/has_target/jps/on_finish_pathing] +#define COMSIG_MOVELOOP_JPS_FINISHED_PATHING "moveloop_jps_finished_pathing" diff --git a/code/controllers/subsystem/movement/movement_types.dm b/code/controllers/subsystem/movement/movement_types.dm index de8e4531236cf..bf4b6a4302917 100644 --- a/code/controllers/subsystem/movement/movement_types.dm +++ b/code/controllers/subsystem/movement/movement_types.dm @@ -323,6 +323,7 @@ turf/avoid, skip_first, subsystem, + diagonal_handling, priority, flags, datum/extra_info, @@ -343,6 +344,7 @@ simulated_only, avoid, skip_first, + diagonal_handling, initial_path) /datum/move_loop/has_target/jps @@ -360,6 +362,8 @@ var/turf/avoid ///Should we skip the first step? This is the tile we're currently on, which breaks some things var/skip_first + ///Whether we replace diagonal movements with cardinal movements or follow through with them + var/diagonal_handling ///A list for the path we're currently following var/list/movement_path ///Cooldown for repathing, prevents spam @@ -373,7 +377,7 @@ . = ..() on_finish_callbacks += CALLBACK(src, PROC_REF(on_finish_pathing)) -/datum/move_loop/has_target/jps/setup(delay, timeout, atom/chasing, repath_delay, max_path_length, minimum_distance, list/access, simulated_only, turf/avoid, skip_first, list/initial_path) +/datum/move_loop/has_target/jps/setup(delay, timeout, atom/chasing, repath_delay, max_path_length, minimum_distance, list/access, simulated_only, turf/avoid, skip_first, diagonal_handling, list/initial_path) . = ..() if(!.) return @@ -384,6 +388,7 @@ src.simulated_only = simulated_only src.avoid = avoid src.skip_first = skip_first + src.diagonal_handling = diagonal_handling movement_path = initial_path?.Copy() /datum/move_loop/has_target/jps/compare_loops(datum/move_loop/loop_type, priority, flags, extra_info, delay, timeout, atom/chasing, repath_delay, max_path_length, minimum_distance, list/access, simulated_only, turf/avoid, skip_first, initial_path) @@ -410,7 +415,7 @@ if(!COOLDOWN_FINISHED(src, repath_cooldown)) return COOLDOWN_START(src, repath_cooldown, repath_delay) - if(SSpathfinder.pathfind(moving, target, max_path_length, minimum_distance, access, simulated_only, avoid, skip_first, on_finish = on_finish_callbacks)) + if(SSpathfinder.pathfind(moving, target, max_path_length, minimum_distance, access, simulated_only, avoid, skip_first, diagonal_handling, on_finish = on_finish_callbacks)) is_pathing = TRUE SEND_SIGNAL(src, COMSIG_MOVELOOP_JPS_REPATH) @@ -418,6 +423,7 @@ /datum/move_loop/has_target/jps/proc/on_finish_pathing(list/path) movement_path = path is_pathing = FALSE + SEND_SIGNAL(src, COMSIG_MOVELOOP_JPS_FINISHED_PATHING, path) /datum/move_loop/has_target/jps/move() if(!length(movement_path)) diff --git a/code/datums/ai/movement/ai_movement_jps.dm b/code/datums/ai/movement/ai_movement_jps.dm index 31fd90ccf53c2..3644869140d7e 100644 --- a/code/datums/ai/movement/ai_movement_jps.dm +++ b/code/datums/ai/movement/ai_movement_jps.dm @@ -4,13 +4,15 @@ /datum/ai_movement/jps max_pathing_attempts = 20 var/maximum_length = AI_MAX_PATH_LENGTH + ///how we deal with diagonal movement, whether we try to avoid them or follow through with them + var/diagonal_flags = DIAGONAL_REMOVE_CLUNKY /datum/ai_movement/jps/start_moving_towards(datum/ai_controller/controller, atom/current_movement_target, min_distance) . = ..() var/atom/movable/moving = controller.pawn var/delay = controller.movement_delay - var/datum/move_loop/loop = SSmove_manager.jps_move(moving, + var/datum/move_loop/has_target/jps/loop = SSmove_manager.jps_move(moving, current_movement_target, delay, repath_delay = 0.5 SECONDS, @@ -18,6 +20,7 @@ minimum_distance = controller.get_minimum_distance(), access = controller.get_access(), subsystem = SSai_movement, + diagonal_handling = diagonal_flags, extra_info = controller, ) @@ -25,6 +28,8 @@ RegisterSignal(loop, COMSIG_MOVELOOP_POSTPROCESS, PROC_REF(post_move)) RegisterSignal(loop, COMSIG_MOVELOOP_JPS_REPATH, PROC_REF(repath_incoming)) + return loop + /datum/ai_movement/jps/proc/repath_incoming(datum/move_loop/has_target/jps/source) SIGNAL_HANDLER var/datum/ai_controller/controller = source.extra_info @@ -35,3 +40,11 @@ /datum/ai_movement/jps/bot max_pathing_attempts = 25 maximum_length = AI_BOT_PATH_LENGTH + diagonal_flags = DIAGONAL_REMOVE_ALL + +/datum/ai_movement/jps/bot/start_moving_towards(datum/ai_controller/controller, atom/current_movement_target, min_distance) + var/datum/move_loop/loop = ..() + var/atom/our_pawn = controller.pawn + if(isnull(our_pawn)) + return + our_pawn.RegisterSignal(loop, COMSIG_MOVELOOP_JPS_FINISHED_PATHING, TYPE_PROC_REF(/mob/living/basic/bot, generate_bot_path)) diff --git a/code/modules/mob/living/basic/bots/_bots.dm b/code/modules/mob/living/basic/bots/_bots.dm index 81d69ce7701c4..5151ec116fc5f 100644 --- a/code/modules/mob/living/basic/bots/_bots.dm +++ b/code/modules/mob/living/basic/bots/_bots.dm @@ -69,6 +69,14 @@ GLOBAL_LIST_INIT(command_strings, list( var/obj/item/card/id/access_card ///The trim type that will grant additional acces var/datum/id_trim/additional_access + ///file the path icon is stored in + var/path_image_icon = 'icons/mob/silicon/aibots.dmi' + ///state of the path icon + var/path_image_icon_state = "path_indicator" + ///what color this path icon will use + var/path_image_color = "#FFFFFF" + ///list of all layed path icons + var/list/current_pathed_turfs = list() ///The type of data HUD the bot uses. Diagnostic by default. var/data_hud_type = DATA_HUD_DIAGNOSTIC_BASIC @@ -91,8 +99,9 @@ GLOBAL_LIST_INIT(command_strings, list( /mob/living/basic/bot/Initialize(mapload) . = ..() - AddElement(/datum/element/relay_attackers) + AddElement(/datum/element/relay_attackers) + RegisterSignal(src, COMSIG_MOVABLE_MOVED, PROC_REF(handle_loop_movement)) RegisterSignal(src, COMSIG_ATOM_WAS_ATTACKED, PROC_REF(after_attacked)) RegisterSignal(src, COMSIG_MOB_TRIED_ACCESS, PROC_REF(attempt_access)) ADD_TRAIT(src, TRAIT_NO_GLIDE, INNATE_TRAIT) @@ -185,6 +194,7 @@ GLOBAL_LIST_INIT(command_strings, list( /mob/living/basic/bot/Destroy() GLOB.bots_list -= src calling_ai_ref = null + clear_path_hud() QDEL_NULL(paicard) QDEL_NULL(pa_system) QDEL_NULL(internal_radio) @@ -261,10 +271,10 @@ GLOBAL_LIST_INIT(command_strings, list( fully_replace_character_name(real_name, new_name) /mob/living/basic/bot/proc/check_access(mob/living/user, obj/item/card/id) - if(!istype(user)) // Non-living mobs shouldn't be manipulating bots (like observes using the botkeeper UI). - return FALSE if(user.has_unlimited_silicon_privilege || isAdminGhostAI(user)) // Silicon and Admins always have access. return TRUE + if(!istype(user)) // Non-living mobs shouldn't be manipulating bots (like observes using the botkeeper UI). + return FALSE if(!length(maints_access_required)) // No requirements to access it. return TRUE if(bot_access_flags & BOT_CONTROL_PANEL_OPEN) // Unlocked. @@ -540,6 +550,7 @@ GLOBAL_LIST_INIT(command_strings, list( access_card.set_access(initial_access) diag_hud_set_botstat() diag_hud_set_botmode() + clear_path_hud() if(bypass_ai_reset || isnull(calling_ai_ref)) return var/mob/living/ai_caller = calling_ai_ref.resolve() @@ -593,9 +604,9 @@ GLOBAL_LIST_INIT(command_strings, list( // Actions received from TGUI /mob/living/basic/bot/ui_act(action, list/params, datum/tgui/ui, datum/ui_state/state) . = ..() - if(. || !isliving(ui.user)) + if(.) return - var/mob/living/the_user = ui.user + var/mob/the_user = ui.user if(!check_access(the_user)) balloon_alert(the_user, "access denied!") return @@ -747,6 +758,7 @@ GLOBAL_LIST_INIT(command_strings, list( speed = 2 diag_hud_set_botmode() + clear_path_hud() /mob/living/basic/bot/Logout() . = ..() diff --git a/code/modules/mob/living/basic/bots/bot_hud.dm b/code/modules/mob/living/basic/bots/bot_hud.dm index d9b429a32bf47..4ed97fe32f775 100644 --- a/code/modules/mob/living/basic/bots/bot_hud.dm +++ b/code/modules/mob/living/basic/bots/bot_hud.dm @@ -37,3 +37,81 @@ holder.icon_state = "hudmove" else holder.icon_state = "" + +///proc that handles drawing and transforming the bot's path onto diagnostic huds +/mob/living/basic/bot/proc/generate_bot_path(datum/move_loop/has_target/jps/source) + SIGNAL_HANDLER + + UnregisterSignal(src, COMSIG_MOVELOOP_JPS_FINISHED_PATHING) + + if(isnull(ai_controller)) + return + + clear_path_hud() + + var/list/path_images = active_hud_list[DIAG_PATH_HUD] + QDEL_LIST(path_images) + + var/list/path_huds_watching_me = list(GLOB.huds[DATA_HUD_DIAGNOSTIC_ADVANCED]) + + var/atom/move_target = ai_controller.current_movement_target + if(move_target != ai_controller.blackboard[BB_BEACON_TARGET]) + return + + var/list/our_path = source.movement_path + if(!length(our_path)) + return + + for(var/datum/atom_hud/hud as anything in path_huds_watching_me) + hud.remove_atom_from_hud(src) + + for(var/index in 1 to our_path.len) + if(index == 1 || index == our_path.len) + continue + var/turf/current_turf = our_path[index] + var/turf/previous_turf = our_path[index - 1] + + var/turf/next_turf = our_path[index + 1] + var/next_direction = get_dir(previous_turf, next_turf) + var/previous_direction = get_dir(current_turf, previous_turf) + + var/image/path_display = image(icon = path_image_icon, loc = current_turf, icon_state = path_image_icon_state, layer = GAME_PLANE, dir = next_direction) + + if((ISDIAGONALDIR(next_direction) && (previous_direction & (NORTH|SOUTH)))) + var/turn_value = (next_direction == SOUTHWEST || next_direction == NORTHEAST) ? 90 : -90 + path_display.transform = path_display.transform.Turn(turn_value) + path_display.transform = path_display.transform.Scale(1, -1) + + path_display.color = path_image_color + path_images += path_display + current_pathed_turfs[current_turf] = path_display + + for(var/datum/atom_hud/hud as anything in path_huds_watching_me) + hud.add_atom_to_hud(src) + +///proc that handles moving along the bot's drawn path +/mob/living/basic/bot/proc/handle_loop_movement(atom/movable/source) + SIGNAL_HANDLER + + if(client || !length(current_pathed_turfs) || isnull(ai_controller)) + return + + var/atom/move_target = ai_controller.current_movement_target + + if(move_target != ai_controller.blackboard[BB_BEACON_TARGET]) + clear_path_hud() + + var/turf/our_turf = get_turf(src) + var/image/target_image = current_pathed_turfs[our_turf] + if(target_image) + animate(target_image, alpha = 0, time = 0.3 SECONDS) + current_pathed_turfs -= our_turf + return + +///proc that handles deleting the bot's drawn path when needed +/mob/living/basic/bot/proc/clear_path_hud() + for(var/turf/index as anything in current_pathed_turfs) + var/image/our_image = current_pathed_turfs[index] + animate(our_image, alpha = 0, time = 0.3 SECONDS) + current_pathed_turfs -= index + diff --git a/code/modules/mob/living/basic/bots/cleanbot/cleanbot.dm b/code/modules/mob/living/basic/bots/cleanbot/cleanbot.dm index 204e6f7ba6236..c19ba69c5ce44 100644 --- a/code/modules/mob/living/basic/bots/cleanbot/cleanbot.dm +++ b/code/modules/mob/living/basic/bots/cleanbot/cleanbot.dm @@ -19,6 +19,7 @@ additional_access = /datum/id_trim/job/janitor possessed_message = "You are a cleanbot! Clean the station to the best of your ability!" ai_controller = /datum/ai_controller/basic_controller/bot/cleanbot + path_image_color = "#993299" ///the bucket used to build us. var/obj/item/reagent_containers/cup/bucket/build_bucket ///Flags indicating what kind of cleanables we should scan for to set as our target to clean. diff --git a/code/modules/mob/living/basic/bots/cleanbot/cleanbot_ai.dm b/code/modules/mob/living/basic/bots/cleanbot/cleanbot_ai.dm index 9d1367ee07595..8007f56f78979 100644 --- a/code/modules/mob/living/basic/bots/cleanbot/cleanbot_ai.dm +++ b/code/modules/mob/living/basic/bots/cleanbot/cleanbot_ai.dm @@ -21,7 +21,7 @@ /datum/ai_planning_subtree/acid_spray, /datum/ai_planning_subtree/use_mob_ability/foam_area, /datum/ai_planning_subtree/salute_authority, - /datum/ai_planning_subtree/find_patrol_beacon, + /datum/ai_planning_subtree/find_patrol_beacon/cleanbot, ) reset_keys = list( BB_ACTIVE_PET_COMMAND, @@ -37,6 +37,7 @@ BB_CLEANABLE_DRAWINGS = CLEANBOT_CLEAN_DRAWINGS, BB_HUNTABLE_TRASH = CLEANBOT_CLEAN_TRASH, ) + ai_traits = PAUSE_DURING_DO_AFTER /datum/ai_planning_subtree/pet_planning/cleanbot/SelectBehaviors(datum/ai_controller/basic_controller/bot/controller, seconds_per_tick) var/mob/living/basic/bot/bot_pawn = controller.pawn @@ -49,12 +50,8 @@ /datum/ai_planning_subtree/cleaning_subtree /datum/ai_planning_subtree/cleaning_subtree/SelectBehaviors(datum/ai_controller/basic_controller/bot/cleanbot/controller, seconds_per_tick) - var/mob/living/basic/bot/cleanbot/bot_pawn = controller.pawn - - if(LAZYLEN(bot_pawn.do_afters)) - return SUBTREE_RETURN_FINISH_PLANNING - if(controller.reachable_key(BB_CLEAN_TARGET, BOT_CLEAN_PATH_LIMIT)) + controller.clear_blackboard_key(BB_BEACON_TARGET) controller.queue_behavior(/datum/ai_behavior/execute_clean, BB_CLEAN_TARGET) return SUBTREE_RETURN_FINISH_PLANNING @@ -62,6 +59,7 @@ final_hunt_list += controller.blackboard[BB_CLEANABLE_DECALS] var/list/flag_list = controller.clean_flags + var/mob/living/basic/bot/cleanbot/bot_pawn = controller.pawn for(var/list_key in flag_list) if(!(bot_pawn.janitor_mode_flags & flag_list[list_key])) continue @@ -70,7 +68,7 @@ controller.queue_behavior(/datum/ai_behavior/find_and_set/in_list/clean_targets, BB_CLEAN_TARGET, final_hunt_list) /datum/ai_behavior/find_and_set/in_list/clean_targets - action_cooldown = 2 SECONDS + action_cooldown = 1 SECONDS /datum/ai_behavior/find_and_set/in_list/clean_targets/search_tactic(datum/ai_controller/controller, locate_paths, search_range) var/list/found = typecache_filter_list(oview(search_range, controller.pawn), locate_paths) @@ -185,6 +183,13 @@ return human_target return null +/datum/ai_planning_subtree/find_patrol_beacon/cleanbot + +/datum/ai_planning_subtree/find_patrol_beacon/cleanbot/SelectBehaviors(datum/ai_controller/basic_controller/bot/controller, seconds_per_tick) + if(controller.blackboard_key_exists(BB_CLEAN_TARGET)) + return + return ..() + /datum/pet_command/point_targeting/clean command_name = "Clean" command_desc = "Command a cleanbot to clean the mess." diff --git a/code/modules/mob/living/basic/bots/medbot/medbot.dm b/code/modules/mob/living/basic/bots/medbot/medbot.dm index bb21555666548..d2d7c8464777d 100644 --- a/code/modules/mob/living/basic/bots/medbot/medbot.dm +++ b/code/modules/mob/living/basic/bots/medbot/medbot.dm @@ -25,6 +25,7 @@ additional_access = /datum/id_trim/job/paramedic announcement_type = /datum/action/cooldown/bot_announcement/medbot + path_image_color = "#d9d9f4" ///anouncements when we find a target to heal var/static/list/wait_announcements = list( @@ -146,8 +147,10 @@ ) RegisterSignal(src, COMSIG_HOSTILE_PRE_ATTACKINGTARGET, PROC_REF(pre_attack)) + if(!HAS_TRAIT(SSstation, STATION_TRAIT_MEDBOT_MANIA) || !mapload || !is_station_level(z)) - return + return INITIALIZE_HINT_LATELOAD + skin = "advanced" update_appearance(UPDATE_OVERLAYS) damage_type_healer = HEAL_ALL_DAMAGE diff --git a/code/modules/mob/living/basic/bots/medbot/medbot_ai.dm b/code/modules/mob/living/basic/bots/medbot/medbot_ai.dm index 54315deda72e4..8d8e3efbfe330 100644 --- a/code/modules/mob/living/basic/bots/medbot/medbot_ai.dm +++ b/code/modules/mob/living/basic/bots/medbot/medbot_ai.dm @@ -192,15 +192,15 @@ . = ..() var/mob/living/living_target = controller.blackboard[target_key] if(QDELETED(living_target)) - finish_action(controller, FALSE) + finish_action(controller, FALSE, target_key) return var/datum/action/cooldown/bot_announcement/announcement = controller.blackboard[BB_ANNOUNCE_ABILITY] if(QDELETED(announcement)) - finish_action(controller, FALSE) + finish_action(controller, FALSE, target_key) return var/text_to_announce = "Medical emergency! [living_target] is in critical condition at [get_area(living_target)]!" announcement.announce(text_to_announce, controller.blackboard[BB_RADIO_CHANNEL]) - finish_action(controller, TRUE) + finish_action(controller, TRUE, target_key) /datum/ai_behavior/announce_patient/finish_action(datum/ai_controller/controller, succeeded, target_key) . = ..() From e375b3a2f664db9e970bfab21605dc9ae8dbf2ae Mon Sep 17 00:00:00 2001 From: orange man <61334995+comfyorange@users.noreply.github.com> Date: Thu, 28 Dec 2023 00:57:53 +1300 Subject: [PATCH 10/44] Automatic changelog for PR #80277 [ci skip] --- html/changelogs/AutoChangeLog-pr-80277.yml | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 html/changelogs/AutoChangeLog-pr-80277.yml diff --git a/html/changelogs/AutoChangeLog-pr-80277.yml b/html/changelogs/AutoChangeLog-pr-80277.yml new file mode 100644 index 0000000000000..9622cadecc108 --- /dev/null +++ b/html/changelogs/AutoChangeLog-pr-80277.yml @@ -0,0 +1,5 @@ +author: "Ben10Omintrix" +delete-after: True +changes: + - rscadd: "basic bots can now display their paths on huds" + - bugfix: "medbots can research healing again" \ No newline at end of file From f77fa385c4ea248b168e09969bfdaaf5d98d0def Mon Sep 17 00:00:00 2001 From: Arturlang <24881678+Arturlang@users.noreply.github.com> Date: Wed, 27 Dec 2023 15:34:13 +0200 Subject: [PATCH 11/44] Makes the turbine core part remove references on deactivation (#80593) ## About The Pull Request The delete the world test was showing that the turbine's non-core parts, the compressor and outlet were causing runtimes, so I made the deactivation of the core remove the references from the secondary turbine parts ## Why It's Good For The Game Less harddels means less lag, hopefully No changelog since nothing player facing --- code/modules/power/turbine/turbine.dm | 2 ++ 1 file changed, 2 insertions(+) diff --git a/code/modules/power/turbine/turbine.dm b/code/modules/power/turbine/turbine.dm index 2ca8b1df7f2ed..06466318025a3 100644 --- a/code/modules/power/turbine/turbine.dm +++ b/code/modules/power/turbine/turbine.dm @@ -512,7 +512,9 @@ /obj/machinery/power/turbine/core_rotor/deactivate_parts() if(all_parts_connected) power_off() + compressor?.rotor = null compressor = null + turbine?.rotor = null turbine = null all_parts_connected = FALSE disconnect_from_network() From e0a7c12ef5c6df52d44b6c6112a02ffc2f4d8d01 Mon Sep 17 00:00:00 2001 From: necromanceranne <40847847+necromanceranne@users.noreply.github.com> Date: Thu, 28 Dec 2023 00:35:58 +1100 Subject: [PATCH 12/44] Spelling Error fix and death knell logging for the Vorpal Scythe (#80587) ## About The Pull Request Fixes a spelling error in the death knell windup chat message for the Vorpal Scythe. Adds logging for starting the windup of a death knell, and on a successful death knell for the Vorpal Scythe. ## Why It's Good For The Game Oops, this was unlogged. Fixes https://github.com/tgstation/tgstation/issues/80458 ## Changelog :cl: spellcheck: Fixes a typo in chat message when starting a Death Knell with the Vorpal Scythe admin: Adds logging for the death knell, both when starts and when it is completed successfully. /:cl: --- .../jobs/job_types/chaplain/chaplain_vorpal_scythe.dm | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/code/modules/jobs/job_types/chaplain/chaplain_vorpal_scythe.dm b/code/modules/jobs/job_types/chaplain/chaplain_vorpal_scythe.dm index 0dffeda57811a..74b1cdcf627d2 100644 --- a/code/modules/jobs/job_types/chaplain/chaplain_vorpal_scythe.dm +++ b/code/modules/jobs/job_types/chaplain/chaplain_vorpal_scythe.dm @@ -139,7 +139,7 @@ If the scythe isn't empowered when you sheath it, you take a heap of damage and var/death_knell_speed_mod = 1 - potential_reaping.visible_message(span_danger("[user] begins to raise [src] arove [potential_reaping]'s [head_name]."), span_userdanger("[user] begins to raise [src], aiming to slice off your [head_name]!")) + potential_reaping.visible_message(span_danger("[user] begins to raise [src] above [potential_reaping]'s [head_name]."), span_userdanger("[user] begins to raise [src], aiming to slice off your [head_name]!")) if(potential_reaping.stat >= UNCONSCIOUS || HAS_TRAIT(potential_reaping, TRAIT_INCAPACITATED)) //if the victim is incapacitated (due to paralysis, a stun, being in staminacrit, etc.), critted, unconscious, or dead, it's much easier to properly behead death_knell_speed_mod *= 0.5 if(potential_reaping.stat != DEAD && potential_reaping.has_status_effect(/datum/status_effect/jitter)) //jittering will make it harder to perform the death knell, even if they're still @@ -149,6 +149,8 @@ If the scythe isn't empowered when you sheath it, you take a heap of damage and if(ispodperson(potential_reaping) || ismonkey(potential_reaping)) //And if they're a podperson or monkey, they can just die. death_knell_speed_mod *= 0.5 + log_combat(user, potential_reaping, "prepared to use [src] to decapitate") + if(do_after(user, 15 SECONDS * death_knell_speed_mod, target = potential_reaping)) playsound(get_turf(potential_reaping), 'sound/weapons/bladeslice.ogg', 250, TRUE) reaped_head.dismember() @@ -165,6 +167,7 @@ If the scythe isn't empowered when you sheath it, you take a heap of damage and ) scythe_empowerment(potential_empowerment) + log_combat(user, potential_reaping, "used [src] to decapitate") if(HAS_MIND_TRAIT(user, TRAIT_MORBID)) //You feel good about yourself, pal? user.add_mood_event("morbid_dismemberment", /datum/mood_event/morbid_dismemberment) From f32a324386ad073725529072820bb53fc57177bb Mon Sep 17 00:00:00 2001 From: orange man <61334995+comfyorange@users.noreply.github.com> Date: Thu, 28 Dec 2023 02:37:08 +1300 Subject: [PATCH 13/44] Automatic changelog for PR #80587 [ci skip] --- html/changelogs/AutoChangeLog-pr-80587.yml | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 html/changelogs/AutoChangeLog-pr-80587.yml diff --git a/html/changelogs/AutoChangeLog-pr-80587.yml b/html/changelogs/AutoChangeLog-pr-80587.yml new file mode 100644 index 0000000000000..a7ac80e338050 --- /dev/null +++ b/html/changelogs/AutoChangeLog-pr-80587.yml @@ -0,0 +1,5 @@ +author: "necromanceranne" +delete-after: True +changes: + - spellcheck: "Fixes a typo in chat message when starting a Death Knell with the Vorpal Scythe" + - admin: "Adds logging for the death knell, both when starts and when it is completed successfully." \ No newline at end of file From 03bbea4d707caa84f7e47fc7e9f160adb0fea9c5 Mon Sep 17 00:00:00 2001 From: SyncIt21 <110812394+SyncIt21@users.noreply.github.com> Date: Wed, 27 Dec 2023 19:08:39 +0530 Subject: [PATCH 14/44] Adds turf checks for mercury & lithium random step logic. (#80586) ## About The Pull Request - Fixes #74092 We not only want to make sure it's a turf but also a ground turf (i.e. something you can stand on and not turfs like lava, water, space etc) so you know you can use your legs to move on that ground. ## Changelog :cl: fix: mercury & lithium will no longer make you randomly move outside of cryotubes or in ground less turfs (space, water, lava etc) /:cl: --- code/modules/reagents/chemistry/reagents/other_reagents.dm | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/code/modules/reagents/chemistry/reagents/other_reagents.dm b/code/modules/reagents/chemistry/reagents/other_reagents.dm index 8f27f4637fb48..d38b165901838 100644 --- a/code/modules/reagents/chemistry/reagents/other_reagents.dm +++ b/code/modules/reagents/chemistry/reagents/other_reagents.dm @@ -1007,7 +1007,7 @@ /datum/reagent/mercury/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, times_fired) . = ..() - if(!HAS_TRAIT(src, TRAIT_IMMOBILIZED) && !isspaceturf(affected_mob.loc)) + if(!HAS_TRAIT(src, TRAIT_IMMOBILIZED) && isturf(affected_mob.loc) && !isgroundlessturf(affected_mob.loc)) step(affected_mob, pick(GLOB.cardinals)) if(SPT_PROB(3.5, seconds_per_tick)) affected_mob.emote(pick("twitch","drool","moan")) @@ -1119,7 +1119,7 @@ /datum/reagent/lithium/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, times_fired) . = ..() - if(!HAS_TRAIT(affected_mob, TRAIT_IMMOBILIZED) && !isspaceturf(affected_mob.loc) && isturf(affected_mob.loc)) + if(!HAS_TRAIT(affected_mob, TRAIT_IMMOBILIZED) && isturf(affected_mob.loc) && !isgroundlessturf(affected_mob.loc)) step(affected_mob, pick(GLOB.cardinals)) if(SPT_PROB(2.5, seconds_per_tick)) affected_mob.emote(pick("twitch","drool","moan")) From 65c97dc9b501c0c8db94879beb76e1efb81ea1de Mon Sep 17 00:00:00 2001 From: orange man <61334995+comfyorange@users.noreply.github.com> Date: Thu, 28 Dec 2023 02:44:06 +1300 Subject: [PATCH 15/44] Automatic changelog for PR #80586 [ci skip] --- html/changelogs/AutoChangeLog-pr-80586.yml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 html/changelogs/AutoChangeLog-pr-80586.yml diff --git a/html/changelogs/AutoChangeLog-pr-80586.yml b/html/changelogs/AutoChangeLog-pr-80586.yml new file mode 100644 index 0000000000000..24ff9e3416eb8 --- /dev/null +++ b/html/changelogs/AutoChangeLog-pr-80586.yml @@ -0,0 +1,4 @@ +author: "SyncIt21" +delete-after: True +changes: + - bugfix: "mercury & lithium will no longer make you randomly move outside of cryotubes or in ground less turfs (space, water, lava etc)" \ No newline at end of file From cdaa1d29a4b1e44140eecc6cac73ceef450ad3f8 Mon Sep 17 00:00:00 2001 From: Ghom <42542238+Ghommie@users.noreply.github.com> Date: Wed, 27 Dec 2023 22:10:32 +0100 Subject: [PATCH 16/44] A "postal workers strike" negative station trait. (can be positive) (#80213) ## About The Pull Request Added a negative station trait, which blocks the mail like it's sunday or a official holiday. However! If it's actually sunday or a official holiday, when the mail would be normally blocked already, instead it'll unblock it. It was done this way because it's easier (station traits are loaded before the events and economy subsystems are initialized) and cooler. ## Why It's Good For The Game Granted there are quite a few station traits around cargo already, features interacting with other features is honestly a good thing. ## Changelog :cl: add: Added a "postal workers strike" negative station trait. In the case of holidays and sunday though, it'll be a "postal system overtime" instead. /:cl: --------- Co-authored-by: Mothblocks <35135081+Mothblocks@users.noreply.github.com> --- code/__DEFINES/wall_dents.dm | 1 + code/datums/station_traits/negative_traits.dm | 24 ++++ .../effects/spawners/random/contraband.dm | 56 ++++++++++ .../effects/spawners/random/entertainment.dm | 9 ++ .../effects/spawners/random/food_or_drink.dm | 14 +++ code/game/objects/items/mail.dm | 86 +++++++++++++- code/game/turfs/closed/walls.dm | 2 - .../events/shuttle_loan/shuttle_loan_datum.dm | 105 +++++++++++++++--- .../events/shuttle_loan/shuttle_loan_event.dm | 15 ++- icons/obj/storage/crates.dmi | Bin 38239 -> 40702 bytes 10 files changed, 285 insertions(+), 27 deletions(-) diff --git a/code/__DEFINES/wall_dents.dm b/code/__DEFINES/wall_dents.dm index 1e4f813849ef6..555f98671c079 100644 --- a/code/__DEFINES/wall_dents.dm +++ b/code/__DEFINES/wall_dents.dm @@ -1,2 +1,3 @@ #define WALL_DENT_HIT 1 #define WALL_DENT_SHOT 2 +#define MAX_DENT_DECALS 15 diff --git a/code/datums/station_traits/negative_traits.dm b/code/datums/station_traits/negative_traits.dm index 154d82cca74a5..b9d11b210e4ed 100644 --- a/code/datums/station_traits/negative_traits.dm +++ b/code/datums/station_traits/negative_traits.dm @@ -17,6 +17,30 @@ /datum/station_trait/distant_supply_lines/on_round_start() SSeconomy.pack_price_modifier *= 1.2 +///A negative trait that stops mail from arriving (or the inverse if on holiday). It also enables a specific shuttle loan situation. +/datum/station_trait/mail_blocked + name = "Postal workers strike" + trait_type = STATION_TRAIT_NEGATIVE + weight = 2 + show_in_report = TRUE + report_message = "Due to an ongoing strike announced by the postal workers union, mail won't be delivered this shift." + +/datum/station_trait/mail_blocked/on_round_start() + //This is either a holiday or sunday... well then, let's flip the situation. + if(SSeconomy.mail_blocked) + name = "Postal system overtime" + report_message = "Despite being a day off, the postal system is working overtime today. Mail will be delivered this shift." + else + var/datum/round_event_control/shuttle_loan/our_event = locate() in SSevents.control + our_event.unavailable_situations -= /datum/shuttle_loan_situation/mail_strike + SSeconomy.mail_blocked = !SSeconomy.mail_blocked + +/datum/station_trait/mail_blocked/hangover/revert() + var/datum/round_event_control/shuttle_loan/our_event = locate() in SSevents.control + our_event.unavailable_situations |= /datum/shuttle_loan_situation/mail_strike + SSeconomy.mail_blocked = !SSeconomy.mail_blocked + return ..() + ///A negative trait that reduces the amount of products available from vending machines throughout the station. /datum/station_trait/vending_shortage name = "Vending products shortage" diff --git a/code/game/objects/effects/spawners/random/contraband.dm b/code/game/objects/effects/spawners/random/contraband.dm index f5bf0f452a35c..ca5acbdbe6767 100644 --- a/code/game/objects/effects/spawners/random/contraband.dm +++ b/code/game/objects/effects/spawners/random/contraband.dm @@ -141,3 +141,59 @@ /obj/item/restraints/legcuffs/beartrap/prearmed = 5, //not really a landmine, but still a good threat /obj/effect/mine/shrapnel = 5, ) + +/obj/effect/spawner/random/contraband/grenades + name = "grenades spawner" + loot = list( + /obj/item/grenade/chem_grenade/metalfoam, + /obj/item/grenade/chem_grenade/cleaner, + /obj/effect/spawner/random/entertainment/colorful_grenades, + /obj/item/grenade/smokebomb, + /obj/item/grenade/chem_grenade/antiweed, + /obj/item/grenade/spawnergrenade/syndiesoap, + /obj/effect/spawner/random/contraband/grenades/dangerous, + ) + +/obj/effect/spawner/random/contraband/grenades/dangerous + name = "dangerous grenades spawner" + loot = list( + /obj/item/grenade/flashbang = 3, + /obj/item/grenade/chem_grenade/teargas = 2, + /obj/item/grenade/iedcasing/spawned = 2, + /obj/item/grenade/empgrenade = 2, + /obj/item/grenade/antigravity = 2, + /obj/effect/spawner/random/contraband/grenades/cluster = 1, + /obj/effect/spawner/random/contraband/grenades/lethal = 1, + ) + +/obj/effect/spawner/random/contraband/grenades/cluster + name = "clusterbusters spawner" + loot = list( + /obj/item/grenade/clusterbuster/smoke = 4, + /obj/item/grenade/clusterbuster/metalfoam = 4, + /obj/item/grenade/clusterbuster/cleaner = 4, + /obj/item/grenade/clusterbuster = 3, + /obj/item/grenade/clusterbuster/teargas = 3, + /obj/item/grenade/clusterbuster/antiweed = 3, + /obj/item/grenade/clusterbuster/soap = 2, + /obj/item/grenade/clusterbuster/emp = 1, + /obj/item/grenade/clusterbuster/spawner_spesscarp = 1, + /obj/item/grenade/clusterbuster/facid = 1, + /obj/item/grenade/clusterbuster/inferno = 1, + /obj/item/grenade/clusterbuster/clf3 = 1, + ) + +/obj/effect/spawner/random/contraband/grenades/lethal + name = "lethal grenades spawner" + loot = list( + /obj/item/grenade/chem_grenade/incendiary = 3, + /obj/item/grenade/chem_grenade/facid = 3, + /obj/item/grenade/chem_grenade/ez_clean = 3, + /obj/item/grenade/chem_grenade/clf3 = 2, + /obj/item/grenade/gluon = 2, + /obj/item/grenade/chem_grenade/holy = 2, + /obj/item/grenade/spawnergrenade/spesscarp = 1, + /obj/item/grenade/spawnergrenade/cat = 1, + /obj/item/grenade/frag = 1, + /obj/item/grenade/chem_grenade/bioterrorfoam = 1, + ) diff --git a/code/game/objects/effects/spawners/random/entertainment.dm b/code/game/objects/effects/spawners/random/entertainment.dm index 1c635f07a5aae..0a61cd1ab1fbb 100644 --- a/code/game/objects/effects/spawners/random/entertainment.dm +++ b/code/game/objects/effects/spawners/random/entertainment.dm @@ -281,3 +281,12 @@ /obj/item/toy/plush/ratplush = 2, /obj/item/toy/plush/narplush = 2, ) + +/obj/effect/spawner/random/entertainment/colorful_grenades + name = "colorful/glitter grenades spawner" + loot = list( + /obj/item/grenade/chem_grenade/glitter/pink, + /obj/item/grenade/chem_grenade/glitter/blue, + /obj/item/grenade/chem_grenade/glitter/white, + /obj/item/grenade/chem_grenade/colorful + ) diff --git a/code/game/objects/effects/spawners/random/food_or_drink.dm b/code/game/objects/effects/spawners/random/food_or_drink.dm index b2639b2df68d0..192914b6e3db6 100644 --- a/code/game/objects/effects/spawners/random/food_or_drink.dm +++ b/code/game/objects/effects/spawners/random/food_or_drink.dm @@ -323,3 +323,17 @@ /obj/item/food/donut/jelly/slimejelly/matcha = 2, /obj/item/food/donut/jelly/slimejelly/trumpet = 2, ) + +/obj/effect/spawner/random/food_or_drink/any_snack_or_beverage + name = "any snack or beverage spawner" + icon_state = "slime_jelly_donut" + loot = list( + /obj/effect/spawner/random/food_or_drink/snack = 6, + /obj/effect/spawner/random/food_or_drink/refreshing_beverage = 6, + /obj/effect/spawner/random/food_or_drink/donuts = 5, + /obj/effect/spawner/random/food_or_drink/donkpockets_single = 5, + /obj/effect/spawner/random/food_or_drink/booze = 4, + /obj/effect/spawner/random/food_or_drink/snack/lizard = 4, + /obj/effect/spawner/random/food_or_drink/jelly_donuts = 3, + /obj/effect/spawner/random/food_or_drink/slime_jelly_donuts = 1, + ) diff --git a/code/game/objects/items/mail.dm b/code/game/objects/items/mail.dm index 3c75a708f9c4c..61c0beed6efcf 100644 --- a/code/game/objects/items/mail.dm +++ b/code/game/objects/items/mail.dm @@ -152,14 +152,18 @@ /obj/item/mail/examine_more(mob/user) . = ..() - var/list/msg = list(span_notice("You notice the postmarking on the front of the mail...")) + if(!postmarked) + . += span_info("This mail has no postmarking of any sort...") + else + . += span_notice("You notice the postmarking on the front of the mail...") var/datum/mind/recipient = recipient_ref.resolve() if(recipient) - msg += "\t[span_info("Certified NT mail for [recipient].")]" + . += span_info("[postmarked ? "Certified NT" : "Uncertfieid"] mail for [recipient].") + else if(postmarked) + . += span_info("Certified mail for [GLOB.station_name].") else - msg += "\t[span_info("Certified mail for [GLOB.station_name].")]" - msg += "\t[span_info("Distribute by hand or via destination tagger using the certified NT disposal system.")]" - return msg + . += span_info("This is a dead letter mail with no recipient.") + . += span_info("Distribute by hand or via destination tagger using the certified NT disposal system.") /// Accepts a mind to initialize goodies for a piece of mail. /obj/item/mail/proc/initialize_for_recipient(datum/mind/recipient) @@ -249,6 +253,8 @@ lid_x = -26 lid_y = 2 paint_jobs = null + ///if it'll show the nt mark on the crate + var/postmarked = TRUE /obj/structure/closet/crate/mail/update_icon_state() . = ..() @@ -259,6 +265,11 @@ else icon_state = "[base_icon_state]sealed" +/obj/structure/closet/crate/mail/update_overlays() + . = ..() + if(postmarked) + . += "mail_nt" + /// Fills this mail crate with N pieces of mail, where N is the lower of the amount var passed, and the maximum capacity of this crate. If N is larger than the number of alive human players, the excess will be junkmail. /obj/structure/closet/crate/mail/proc/populate(amount) var/mail_count = min(amount, storage_capacity) @@ -304,6 +315,19 @@ . = ..() populate(INFINITY) +///Used in the mail strike shuttle loan event +/obj/structure/closet/crate/mail/full/mail_strike + desc = "A post crate from somewhere else. It has no NT logo on it." + postmarked = FALSE + +/obj/structure/closet/crate/mail/full/mail_strike/populate(amount) + var/strike_mail_to_spawn = rand(1, storage_capacity-1) + for(var/i in 1 to strike_mail_to_spawn) + if(prob(95)) + new /obj/item/mail/mail_strike(src) + else + new /obj/item/mail/traitor/mail_strike(src) + return ..(storage_capacity - strike_mail_to_spawn) /// Opened mail crate /obj/structure/closet/crate/mail/preopen @@ -381,7 +405,8 @@ playsound(loc, 'sound/items/poster_ripped.ogg', vol = 50, vary = TRUE) for(var/obj/item/stuff as anything in contents) // Mail and envelope actually can have more than 1 item. if(user.put_in_hands(stuff) && armed) - log_bomber(user, "opened armed mail made by [made_by_cached_name] ([made_by_cached_ckey]), activating", stuff) + var/whomst = made_by_cached_name ? "[made_by_cached_name] ([made_by_cached_ckey])" : "no one in particular" + log_bomber(user, "opened armed mail made by [whomst], activating", stuff) INVOKE_ASYNC(stuff, TYPE_PROC_REF(/obj/item, attack_self), user) qdel(src) return TRUE @@ -412,6 +437,55 @@ after_unwrap(user) return TRUE +///Generic mail used in the mail strike shuttle loan event +/obj/item/mail/mail_strike + name = "dead mail" + desc = "An unmarked parcel of unknown origins, effectively undeliverable." + postmarked = FALSE + generic_goodies = list( + /obj/effect/spawner/random/entertainment/money_medium = 2, + /obj/effect/spawner/random/contraband = 2, + /obj/effect/spawner/random/entertainment/money_large = 1, + /obj/effect/spawner/random/entertainment/coin = 1, + /obj/effect/spawner/random/food_or_drink/any_snack_or_beverage = 1, + /obj/effect/spawner/random/entertainment/drugs = 1, + /obj/effect/spawner/random/contraband/grenades = 1, + ) + +/obj/item/mail/mail_strike/Initialize(mapload) + if(prob(35)) + stamped = FALSE + if(prob(35)) + name = "dead envelope" + icon_state = "mail_large" + goodie_count = 2 + stamp_max = 2 + stamp_offset_y = 5 + . = ..() + color = pick(COLOR_SILVER, COLOR_DARK, COLOR_DRIED_TAN, COLOR_ORANGE_BROWN, COLOR_BROWN, COLOR_SYNDIE_RED) + for(var/goodie in 1 to goodie_count) + var/target_good = pick_weight(generic_goodies) + new target_good(src) + +///Also found in the mail strike shuttle loan. It contains a random grenade that'll be triggered when unwrapped +/obj/item/mail/traitor/mail_strike + name = "dead mail" + desc = "An unmarked parcel of unknown origins, effectively undeliverable." + postmarked = FALSE + +/obj/item/mail/traitor/mail_strike/Initialize(mapload) + if(prob(35)) + stamped = FALSE + if(prob(35)) + name = "dead envelope" + icon_state = "mail_large" + goodie_count = 2 + stamp_max = 2 + stamp_offset_y = 5 + . = ..() + color = pick(COLOR_SILVER, COLOR_DARK, COLOR_DRIED_TAN, COLOR_ORANGE_BROWN, COLOR_BROWN, COLOR_SYNDIE_RED) + new /obj/effect/spawner/random/contraband/grenades/dangerous(src) + /obj/item/storage/mail_counterfeit_device name = "GLA-2 mail counterfeit device" desc = "Device that actually able to counterfeit NT's mail. This device also able to place a trap inside of mail for malicious actions. Trap will \"activate\" any item inside of mail. Also it might be used for contraband purposes. Integrated micro-computer will give you great configuration optionality for your needs." diff --git a/code/game/turfs/closed/walls.dm b/code/game/turfs/closed/walls.dm index d4ff7b38ef4ba..e660e95b72e11 100644 --- a/code/game/turfs/closed/walls.dm +++ b/code/game/turfs/closed/walls.dm @@ -1,4 +1,3 @@ -#define MAX_DENT_DECALS 15 #define LEANING_OFFSET 11 /turf/closed/wall @@ -387,5 +386,4 @@ . = ..() SEND_SIGNAL(gone, COMSIG_LIVING_WALL_EXITED, src) -#undef MAX_DENT_DECALS #undef LEANING_OFFSET diff --git a/code/modules/events/shuttle_loan/shuttle_loan_datum.dm b/code/modules/events/shuttle_loan/shuttle_loan_datum.dm index b0c8775cba25c..706274dfd85bf 100644 --- a/code/modules/events/shuttle_loan/shuttle_loan_datum.dm +++ b/code/modules/events/shuttle_loan/shuttle_loan_datum.dm @@ -7,7 +7,7 @@ /// What the shuttle says about it. var/shuttle_transit_text = "Unset transit text" /// Supply points earned for taking the deal. - var/bonus_points = 10000 + var/bonus_points = CARGO_CRATE_VALUE * 50 /// Response for taking the deal. var/thanks_msg = "The cargo shuttle should return in five minutes. Have some supply points for your trouble." /// Small description of the loan for easier log reading. @@ -21,7 +21,7 @@ bonus_points *= 1.15 /// Spawns paths added to `spawn_list`, and passes empty shuttle turfs so you can spawn more complicated things like dead bodies. -/datum/shuttle_loan_situation/proc/spawn_items(list/spawn_list, list/empty_shuttle_turfs) +/datum/shuttle_loan_situation/proc/spawn_items(list/spawn_list, list/empty_shuttle_turfs, list/blocked_shutte_turfs) SHOULD_CALL_PARENT(FALSE) CRASH("Unimplemented get_spawned_items() on [src.type].") @@ -31,7 +31,7 @@ shuttle_transit_text = "Virus samples incoming." logging_desc = "Virus shuttle" -/datum/shuttle_loan_situation/antidote/spawn_items(list/spawn_list, list/empty_shuttle_turfs) +/datum/shuttle_loan_situation/antidote/spawn_items(list/spawn_list, list/empty_shuttle_turfs, list/blocked_shutte_turfs) var/obj/effect/mob_spawn/corpse/human/assistant/infected_assistant = pick(list( /obj/effect/mob_spawn/corpse/human/assistant/beesease_infection, /obj/effect/mob_spawn/corpse/human/assistant/brainrot_infection, @@ -58,7 +58,7 @@ bonus_points = 0 logging_desc = "Resupply packages" -/datum/shuttle_loan_situation/department_resupply/spawn_items(list/spawn_list, list/empty_shuttle_turfs) +/datum/shuttle_loan_situation/department_resupply/spawn_items(list/spawn_list, list/empty_shuttle_turfs, list/blocked_shutte_turfs) var/list/crate_types = list( /datum/supply_pack/emergency/equipment, /datum/supply_pack/security/supplies, @@ -84,7 +84,7 @@ shuttle_transit_text = "Syndicate hijack team incoming." logging_desc = "Syndicate boarding party" -/datum/shuttle_loan_situation/syndiehijacking/spawn_items(list/spawn_list, list/empty_shuttle_turfs) +/datum/shuttle_loan_situation/syndiehijacking/spawn_items(list/spawn_list, list/empty_shuttle_turfs, list/blocked_shutte_turfs) var/datum/supply_pack/pack = SSshuttle.supply_packs[/datum/supply_pack/imports/specialops] pack.generate(pick_n_take(empty_shuttle_turfs)) @@ -99,10 +99,10 @@ sender = "CentCom Janitorial Division" announcement_text = "One of our freighters carrying a bee shipment has been attacked by eco-terrorists. Can you clean up the mess for us?" shuttle_transit_text = "Biohazard cleanup incoming." - bonus_points = 20000 //Toxin bees can be unbeelievably lethal + bonus_points = CARGO_CRATE_VALUE * 100 //Toxin bees can be unbeelievably lethal logging_desc = "Shuttle full of bees" -/datum/shuttle_loan_situation/lots_of_bees/spawn_items(list/spawn_list, list/empty_shuttle_turfs) +/datum/shuttle_loan_situation/lots_of_bees/spawn_items(list/spawn_list, list/empty_shuttle_turfs, list/blocked_shutte_turfs) var/datum/supply_pack/pack = SSshuttle.supply_packs[/datum/supply_pack/organic/hydroponics/beekeeping_fullkit] pack.generate(pick_n_take(empty_shuttle_turfs)) @@ -135,10 +135,10 @@ announcement_text = "We have discovered an active Syndicate bomb near our VIP shuttle's fuel lines. If you feel up to the task, we will pay you for defusing it." shuttle_transit_text = "Live explosive ordnance incoming. Exercise extreme caution." thanks_msg = "Live explosive ordnance incoming via supply shuttle. Evacuating cargo bay is recommended." - bonus_points = 45000 //If you mess up, people die and the shuttle gets turned into swiss cheese + bonus_points = CARGO_CRATE_VALUE * 225 //If you mess up, people die and the shuttle gets turned into swiss cheese logging_desc = "Shuttle with a ticking bomb" -/datum/shuttle_loan_situation/jc_a_bomb/spawn_items(list/spawn_list, list/empty_shuttle_turfs) +/datum/shuttle_loan_situation/jc_a_bomb/spawn_items(list/spawn_list, list/empty_shuttle_turfs, list/blocked_shutte_turfs) spawn_list.Add(/obj/machinery/syndicatebomb/shuttle_loan) if(prob(95)) spawn_list.Add(/obj/item/paper/fluff/cargo/bomb) @@ -153,7 +153,7 @@ bonus_points = 0 //Payout is made when the stamped papers are returned logging_desc = "Paperwork shipment" -/datum/shuttle_loan_situation/papers_please/spawn_items(list/spawn_list, list/empty_shuttle_turfs) +/datum/shuttle_loan_situation/papers_please/spawn_items(list/spawn_list, list/empty_shuttle_turfs, list/blocked_shutte_turfs) spawn_list += subtypesof(/obj/item/paperwork) - typesof(/obj/item/paperwork/photocopy) - typesof(/obj/item/paperwork/ancient) /datum/shuttle_loan_situation/pizza_delivery @@ -164,7 +164,7 @@ bonus_points = 0 logging_desc = "Pizza delivery" -/datum/shuttle_loan_situation/pizza_delivery/spawn_items(list/spawn_list, list/empty_shuttle_turfs) +/datum/shuttle_loan_situation/pizza_delivery/spawn_items(list/spawn_list, list/empty_shuttle_turfs, list/blocked_shutte_turfs) var/naughtypizza = list(/obj/item/pizzabox/bomb, /obj/item/pizzabox/margherita/robo) //oh look another blacklist, for pizza nonetheless! var/nicepizza = list(/obj/item/pizzabox/margherita, /obj/item/pizzabox/meat, /obj/item/pizzabox/vegetable, /obj/item/pizzabox/mushroom) for(var/i in 1 to 6) @@ -176,7 +176,7 @@ shuttle_transit_text = "Partying Russians incoming." logging_desc = "Russian party squad" -/datum/shuttle_loan_situation/russian_party/spawn_items(list/spawn_list, list/empty_shuttle_turfs) +/datum/shuttle_loan_situation/russian_party/spawn_items(list/spawn_list, list/empty_shuttle_turfs, list/blocked_shutte_turfs) var/datum/supply_pack/pack = SSshuttle.supply_packs[/datum/supply_pack/service/party] pack.generate(pick_n_take(empty_shuttle_turfs)) @@ -194,7 +194,7 @@ shuttle_transit_text = "Spider Clan gift incoming." logging_desc = "Shuttle full of spiders" -/datum/shuttle_loan_situation/spider_gift/spawn_items(list/spawn_list, list/empty_shuttle_turfs) +/datum/shuttle_loan_situation/spider_gift/spawn_items(list/spawn_list, list/empty_shuttle_turfs, list/blocked_shutte_turfs) var/datum/supply_pack/pack = SSshuttle.supply_packs[/datum/supply_pack/imports/specialops] pack.generate(pick_n_take(empty_shuttle_turfs)) @@ -213,3 +213,82 @@ for(var/i in 1 to 5) var/turf/web_turf = pick_n_take(empty_shuttle_turfs) new /obj/structure/spider/stickyweb(web_turf) + +#define DENT_WALL "dent" +#define CHANGE_WALL "change" +#define DISMANTLE_WALL "dismantle" + +#define BREAK_TILE "break" +#define PLATING_TILE "plating" +#define RUST_TILE "rust" + +/** + * A special shuttle loan situation enabled by the 'mail blocked' station trait. + * It sends back a lot of mail to the station, at the cost of wrecking the cargo shuttle a little. + */ +/datum/shuttle_loan_situation/mail_strike + sender = "Spinward Mail Workers Union" + announcement_text = "The Mail Workers Union wants to borrow your cargo shuttle to employ \"advanced union strike tactics\" with. Payment is strictly in mails." + bonus_points = 0 + thanks_msg = "The cargo shuttle should return in five minutes." + shuttle_transit_text = "Nothing stops the mail." + logging_desc = "Shuttle full of shady mail" + +/datum/shuttle_loan_situation/mail_strike/spawn_items(list/spawn_list, list/empty_shuttle_turfs, list/blocked_shutte_turfs) + for(var/i in 1 to rand(7, 12)) + var/turf/closed/wall/wall = pick_n_take(blocked_shutte_turfs) + if(!istype(wall)) + continue + var/static/list/wall_bad_stuff = list(DENT_WALL = 85, CHANGE_WALL = 13, DISMANTLE_WALL = 2) + var/static/list/possible_new_walls = list( + /turf/closed/wall/mineral/sandstone, + /turf/closed/wall/mineral/wood, + /turf/closed/wall/mineral/iron, + /turf/closed/wall/metal_foam_base, + /turf/closed/wall/r_wall, + ) + var/damage_done = pick_weight(wall_bad_stuff) + switch(damage_done) + if(DENT_WALL) + for(var/dent in 1 to rand(1, MAX_DENT_DECALS)) + wall.add_dent(prob(90) ? WALL_DENT_SHOT : WALL_DENT_HIT) + if(CHANGE_WALL) + wall.ChangeTurf(pick(possible_new_walls - wall.type)) + if(prob(25)) + for(var/dent in 1 to rand(1, MAX_DENT_DECALS)) + wall.add_dent(prob(90) ? WALL_DENT_SHOT : WALL_DENT_HIT) + if(DISMANTLE_WALL) + wall.dismantle_wall() + + for(var/i in 1 to rand(7, 12)) + var/turf/open/floor/floor = pick_n_take(empty_shuttle_turfs) + if(!istype(floor)) + continue + var/static/list/floor_bad_stuff = list(BREAK_TILE = 65, PLATING_TILE = 25, RUST_TILE = 10) + var/damage_done = pick_weight(floor_bad_stuff) + switch(damage_done) + if(BREAK_TILE) + if(prob(50)) + floor.break_tile() + else + floor.burn_tile() + if(PLATING_TILE) + if(prob(25)) + floor.remove_tile() + else + floor.make_plating() + if(RUST_TILE) + floor.ChangeTurf(/turf/open/floor/plating/rust) + if(prob(25)) + spawn_list += pick(/obj/effect/gibspawner/robot, /obj/effect/gibspawner/human) + + for(var/i in 1 to rand(4, 7)) + spawn_list += /obj/structure/closet/crate/mail/full/mail_strike + +#undef BREAK_TILE +#undef PLATING_TILE +#undef RUST_TILE + +#undef DENT_WALL +#undef CHANGE_WALL +#undef DISMANTLE_WALL diff --git a/code/modules/events/shuttle_loan/shuttle_loan_event.dm b/code/modules/events/shuttle_loan/shuttle_loan_event.dm index 6eeddea021407..99e70e394953b 100644 --- a/code/modules/events/shuttle_loan/shuttle_loan_event.dm +++ b/code/modules/events/shuttle_loan/shuttle_loan_event.dm @@ -8,7 +8,8 @@ description = "If cargo accepts the offer, fills the shuttle with loot and/or enemies." ///The types of loan events already run (and to be excluded if the event triggers). admin_setup = list(/datum/event_admin_setup/listed_options/shuttle_loan) - var/list/run_situations = list() + ///A list of normally unavailable (or already run) situations datums + var/list/unavailable_situations = list(/datum/shuttle_loan_situation/mail_strike) /datum/round_event_control/shuttle_loan/can_spawn_event(players_amt, allow_magic = FALSE) . = ..() @@ -28,14 +29,14 @@ var/datum/round_event_control/shuttle_loan/loan_control = control //by this point if situation is admin picked, it is a type, not an instance. if(!situation) - var/list/valid_situations = subtypesof(/datum/shuttle_loan_situation) - loan_control.run_situations + var/list/valid_situations = subtypesof(/datum/shuttle_loan_situation) - loan_control.unavailable_situations if(!valid_situations.len) //If we somehow run out of loans (fking campbell), they all become available again - loan_control.run_situations.Cut() + loan_control.unavailable_situations.Cut() valid_situations = subtypesof(/datum/shuttle_loan_situation) situation = pick(valid_situations) - loan_control.run_situations.Add(situation) + loan_control.unavailable_situations.Add(situation) situation = new situation() /datum/round_event/shuttle_loan/announce(fake) @@ -76,10 +77,12 @@ //get empty turfs var/list/empty_shuttle_turfs = list() + var/list/blocked_shutte_turfs = list() var/list/area/shuttle/shuttle_areas = SSshuttle.supply.shuttle_areas for(var/area/shuttle/shuttle_area as anything in shuttle_areas) for(var/turf/open/floor/shuttle_turf in shuttle_area) if(shuttle_turf.is_blocked_turf()) + blocked_shutte_turfs += shuttle_turf continue empty_shuttle_turfs += shuttle_turf if(!empty_shuttle_turfs.len) @@ -87,7 +90,7 @@ //let the situation spawn its items var/list/spawn_list = list() - situation.spawn_items(spawn_list, empty_shuttle_turfs) + situation.spawn_items(spawn_list, empty_shuttle_turfs, blocked_shutte_turfs) var/false_positive = 0 while(spawn_list.len && empty_shuttle_turfs.len) @@ -103,7 +106,7 @@ /datum/event_admin_setup/listed_options/shuttle_loan/get_list() var/datum/round_event_control/shuttle_loan/loan_event = event_control - var/list/valid_situations = subtypesof(/datum/shuttle_loan_situation) - loan_event.run_situations + var/list/valid_situations = subtypesof(/datum/shuttle_loan_situation) - loan_event.unavailable_situations return valid_situations /datum/event_admin_setup/listed_options/shuttle_loan/apply_to_event(datum/round_event/shuttle_loan/event) diff --git a/icons/obj/storage/crates.dmi b/icons/obj/storage/crates.dmi index 274af66c820a482c6798b6fb1bc0ec8e169b8919..9bc8f4d2c27e90caca91cd6b22ec035d1fdc34a7 100644 GIT binary patch literal 40702 zcmdSA_di@;^e=n{iC!Z_mq>z$5~7z8(V~;+VU!4>chN={qC`Y*K@ibeb%YhmrZxhpbonB)}P76W>}-_`s*YO)aIc%cF3G5n7E3cw{l= z8IkZ~eN==zu1+9ho0d@fm>8km+YzG0OlrX4=*q}VJ>@W!`$jAzWR_c3KCWZc{9XmI zc4*)c;lou6-PAzhr#sIgXYR~~uMcr5(U~v?xT_32bnGMHdw1@l;U4*0G=h0HQ28!0Dw|1taM4f}mBp*Sf6 zM{nyH$-h+JN6aoPnzL!6k2WAvuT0aNs`F6vOp4tM!42|pcxfuf`%^ko1>_lltNjs; zo(KaK0+RhU^SYl+F^Lz3vXJqPa{0@{PcrZKbh(1ezLe8!uUJpWNYW;7N)1$N#Q64W z5-lavlzjCiB7UAyP7z!TXc`FB~&M`R7Y_#B!B2_7jeco13788?}?W&>+oqYqDI9E&yN! zUdg_A827)M=1wg&c?yE}j#fvg0+OB*|66nHNv@RoeLA^$Zo$&dly^@~jkXW*ubOxPD( z>6p^vr~8G_M55(cu0J-JbwsF5{h>VlC!;BGaS z50Y)u=E8hqXoFusK(oytb^P}k#5oH)`b=A}=1 z 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 Fe*t&G36cN+ literal 38239 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 UFLhv2z Date: Thu, 28 Dec 2023 10:11:02 +1300 Subject: [PATCH 17/44] Automatic changelog for PR #80213 [ci skip] --- html/changelogs/AutoChangeLog-pr-80213.yml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 html/changelogs/AutoChangeLog-pr-80213.yml diff --git a/html/changelogs/AutoChangeLog-pr-80213.yml b/html/changelogs/AutoChangeLog-pr-80213.yml new file mode 100644 index 0000000000000..feb4a5b6cfa21 --- /dev/null +++ b/html/changelogs/AutoChangeLog-pr-80213.yml @@ -0,0 +1,4 @@ +author: "Ghommie" +delete-after: True +changes: + - rscadd: "Added a \"postal workers strike\" negative station trait. In the case of holidays and sunday though, it'll be a \"postal system overtime\" instead." \ No newline at end of file From 5a816310892ca83502ae72f9cc3430799f6f0efe Mon Sep 17 00:00:00 2001 From: LemonInTheDark <58055496+LemonInTheDark@users.noreply.github.com> Date: Wed, 27 Dec 2023 13:35:29 -0800 Subject: [PATCH 18/44] Reverts Canister Wiring (#80265) (#80607) ## About The Pull Request Reverts #80265 It's a cool idea, but the ability to toggle valves/control pressure make this just a ttv but without the access issues we tie to that. Eats into that space way too much. Removing those but not the rest results in disappointment, so I'm just full removing this. I am keeping the code quality changes, screentips, that sort of thing tho. That bit goes hard. Also, refactored a few procs slightly to make them easier to read. Early returns, sane loops, etc. ## Why It's Good For The Game Closes #80604 Cheap remote mass producible maxcaps are a bad idea actually Sorry I didn't catch this in review, been resting for the week ## Changelog :cl: del: Removes the wires from canisters. It's a cool idea, but cheap controlled maxcaps are bad actually /:cl: --- code/__DEFINES/wires.dm | 6 - code/datums/wires/canister.dm | 33 ---- .../machinery/portable/canister.dm | 151 ++++++------------ tgstation.dme | 1 - 4 files changed, 45 insertions(+), 146 deletions(-) delete mode 100644 code/datums/wires/canister.dm diff --git a/code/__DEFINES/wires.dm b/code/__DEFINES/wires.dm index 633dcc360c2c8..2b4c528abc212 100644 --- a/code/__DEFINES/wires.dm +++ b/code/__DEFINES/wires.dm @@ -64,12 +64,6 @@ #define WIRE_ZAP1 "High Voltage Circuit 1" #define WIRE_ZAP2 "High Voltage Circuit 2" #define WIRE_OVERCLOCK "Overclock" -#define WIRE_VALVE "Valve" -#define WIRE_SHIELDING "Shielding" -#define WIRE_REGULATOR_MIN "Minimize Regulator Pressure" -#define WIRE_REGULATOR_MAX "Maximize Regulator Pressure" -#define WIRE_TANK_EJECT "Eject Tank" -#define WIRE_REACTION_SUPPRESSION "Reaction Suppression" // Wire states for the AI #define AI_WIRE_NORMAL 0 diff --git a/code/datums/wires/canister.dm b/code/datums/wires/canister.dm deleted file mode 100644 index c692ff22872bf..0000000000000 --- a/code/datums/wires/canister.dm +++ /dev/null @@ -1,33 +0,0 @@ -/datum/wires/canister - holder_type = /obj/machinery/portable_atmospherics/canister - proper_name = "Canister" - -/datum/wires/canister/New(atom/holder) - wires = list(WIRE_VALVE, WIRE_SHIELDING, WIRE_REGULATOR_MIN, WIRE_REGULATOR_MAX, WIRE_TANK_EJECT, WIRE_REACTION_SUPPRESSION) - ..() - -/datum/wires/canister/on_pulse(wire) - var/obj/machinery/portable_atmospherics/canister/the_canister = holder - if(!the_canister.internal_cell) - return - switch(wire) - if(WIRE_VALVE) - the_canister.toggle_valve(usr, wire_pulsed = TRUE) - if(WIRE_SHIELDING) - the_canister.toggle_shielding(usr, wire_pulsed = TRUE) - if(WIRE_TANK_EJECT) - the_canister.eject_tank(usr, wire_pulsed = TRUE) - if(WIRE_REGULATOR_MIN) - the_canister.release_pressure = CAN_MIN_RELEASE_PRESSURE - the_canister.investigate_log("was set to [the_canister.release_pressure] kPa by [key_name(usr)] via wire pulse.", INVESTIGATE_ATMOS) - if(WIRE_REGULATOR_MAX) - the_canister.release_pressure = CAN_MAX_RELEASE_PRESSURE - the_canister.investigate_log("was set to [the_canister.release_pressure] kPa by [key_name(usr)] via wire pulse.", INVESTIGATE_ATMOS) - if(WIRE_REACTION_SUPPRESSION) - the_canister.toggle_reaction_suppression(usr, wire_pulsed = TRUE) - -/datum/wires/canister/can_reveal_wires(mob/user) - if(HAS_TRAIT(user, TRAIT_KNOW_ENGI_WIRES)) - return TRUE - - return ..() diff --git a/code/modules/atmospherics/machinery/portable/canister.dm b/code/modules/atmospherics/machinery/portable/canister.dm index bef8eb7300d87..18fe9817e8115 100644 --- a/code/modules/atmospherics/machinery/portable/canister.dm +++ b/code/modules/atmospherics/machinery/portable/canister.dm @@ -38,12 +38,6 @@ var/obj/item/stock_parts/cell/internal_cell ///used while processing to update appearance only when its pressure state changes var/current_pressure_state - /// An assembly attached to the canister - var/obj/item/assembly_holder/rig = null - /// The person who attached an assembly to this canister, usually for ignition purposes in tandem with valve wire pulse - var/mob/last_rigger = "" - //overlay of attached assemblies - var/mutable_appearance/assemblies_overlay /datum/armor/portable_atmospherics_canister melee = 50 @@ -60,8 +54,6 @@ if(mapload) internal_cell = new /obj/item/stock_parts/cell/high(src) - set_wires(new /datum/wires/canister(src)) - if(existing_mixture) air_contents.copy_from(existing_mixture) else @@ -81,17 +73,6 @@ AddComponent(/datum/component/gas_leaker, leak_rate=0.01) register_context() -/obj/machinery/portable_atmospherics/canister/Destroy() - QDEL_NULL(wires) - return ..() - -/obj/machinery/portable_atmospherics/canister/Exited(atom/movable/gone) - if(gone == rig) - rig = null - last_rigger = null - cut_overlays(assemblies_overlay) - return ..() - /obj/machinery/portable_atmospherics/canister/interact(mob/user) . = ..() if(!allowed(user)) @@ -101,14 +82,10 @@ /obj/machinery/portable_atmospherics/canister/add_context(atom/source, list/context, obj/item/held_item, mob/user) . = ..() - if(rig) - context[SCREENTIP_CONTEXT_RMB] = "Remove rigged device" if(holding) context[SCREENTIP_CONTEXT_ALT_LMB] = "Remove tank" if(!held_item) return CONTEXTUAL_SCREENTIP_SET - if(istype(held_item, /obj/item/assembly_holder)) - context[SCREENTIP_CONTEXT_LMB] = "Rig" if(istype(held_item, /obj/item/stock_parts/cell)) context[SCREENTIP_CONTEXT_LMB] = "Insert cell" switch(held_item.tool_behaviour) @@ -132,11 +109,6 @@ . += span_notice("Warning, no cell installed, use a screwdriver to open the hatch and insert one.") if(panel_open) . += span_notice("Hatch open, close it with a screwdriver.") - if(get_dist(user, src) <= 2) - if(rig) - . += span_warning("There is some kind of device rigged to the canister!") - else - . += span_notice("It looks like you could rig a device to the canister.") // Please keep the canister types sorted // Basic canister per gas below here @@ -411,50 +383,8 @@ balloon_alert(user, "you install the cell") internal_cell = active_cell return TRUE - if(is_wire_tool(item) && panel_open) - wires.interact(user) - return - if(istype(item, /obj/item/assembly_holder)) - if(rig) - balloon_alert(user, "another device is in the way!") - return ..() - var/obj/item/assembly_holder/holder = item - - user.balloon_alert_to_viewers("attaching rig...") - add_fingerprint(user) - if(!do_after(user, 2 SECONDS, target = src) || !user.transferItemToLoc(holder, src)) - return - rig = holder - holder.master = src - holder.on_attach() - assemblies_overlay = holder - assemblies_overlay.layer = 3.3 - assemblies_overlay.pixel_y -= 5 - add_overlay(assemblies_overlay) - log_bomber(user, "attached [holder.name] to ", src) - last_rigger = user - user.balloon_alert_to_viewers("attached rig") - return - return ..() -/obj/machinery/portable_atmospherics/canister/attack_hand_secondary(mob/user, list/modifiers) - if(!rig) - return - // mousetrap rigs only make sense if you can set them off, can't step on them - // If you see a mousetrap-rigged fuel tank, just leave it alone - rig.on_found() - if(QDELETED(src)) - return - user.balloon_alert_to_viewers("detaching rig...") - if(!do_after(user, 2 SECONDS, target = src)) - return - user.balloon_alert_to_viewers("detached rig") - user.log_message("detached [rig] from [src].", LOG_GAME) - if(!user.put_in_hands(rig)) - rig.forceMove(get_turf(user)) - return SECONDARY_ATTACK_CANCEL_ATTACK_CHAIN - /obj/machinery/portable_atmospherics/canister/screwdriver_act(mob/living/user, obj/item/screwdriver) if(default_deconstruction_screwdriver(user, icon_state, icon_state, screwdriver)) update_appearance() @@ -723,39 +653,47 @@ update_appearance() - /// Opens/closes the canister valve /obj/machinery/portable_atmospherics/canister/proc/toggle_valve(mob/user, wire_pulsed = FALSE) - var/logmsg - var/admin_msg - var/danger = FALSE - var/n = 0 valve_open = !valve_open - if(valve_open) - SSair.start_processing_machine(src) - logmsg = "Valve was opened by [key_name(user)] [wire_pulsed ? "via wire pulse" : ""], starting a transfer into \the [holding || "air"].
" - if(!holding) - var/list/gaseslog = list() //list for logging all gases in canister - for(var/id in air_contents.gases) - var/gas = air_contents.gases[id] - gaseslog[gas[GAS_META][META_GAS_NAME]] = gas[MOLES] //adds gases to gaseslog - if(!gas[GAS_META][META_GAS_DANGER]) - continue - if(gas[MOLES] > (gas[GAS_META][META_GAS_MOLES_VISIBLE] || MOLES_GAS_VISIBLE)) //if moles_visible is undefined, default to default visibility - danger = TRUE //at least 1 danger gas - logmsg = "[key_name(user)] opened a canister [wire_pulsed ? "via wire pulse" : ""] that contains the following:" - admin_msg = "[ADMIN_LOOKUPFLW(user)] opened a canister [wire_pulsed ? "via wire pulse" : ""] that contains the following at [ADMIN_VERBOSEJMP(src)]:" - for(var/name in gaseslog) - n = n + 1 - logmsg += "\n[name]: [gaseslog[name]] moles." - if(n <= 5) //the first five gases added - admin_msg += "\n[name]: [gaseslog[name]] moles." - if(n == 5 && length(gaseslog) > 5) //message added if more than 5 gases - admin_msg += "\nToo many gases to log. Check investigate log." - if(danger) //sent to admin's chat if contains dangerous gases - message_admins(admin_msg) - else - logmsg = "valve was closed by [key_name(user)] [wire_pulsed ? "via wire pulse" : ""], stopping the transfer into \the [holding || "air"].
" + if(!valve_open) + var/logmsg = "valve was closed by [key_name(user)] [wire_pulsed ? "via wire pulse" : ""], stopping the transfer into \the [holding || "air"].
" + investigate_log(logmsg, INVESTIGATE_ATMOS) + release_log += logmsg + return + + SSair.start_processing_machine(src) + if(holding) + var/logmsg = "Valve was opened by [key_name(user)] [wire_pulsed ? "via wire pulse" : ""], starting a transfer into \the [holding || "air"].
" + investigate_log(logmsg, INVESTIGATE_ATMOS) + release_log += logmsg + return + + // Go over the gases in canister, pull all their info and mark the spooky ones + var/list/output = list() + output += "[key_name(user)] opened a canister [wire_pulsed ? "via wire pulse" : ""] that contains the following:" + var/list/admin_output = list() + admin_output += "[ADMIN_LOOKUPFLW(user)] opened a canister [wire_pulsed ? "via wire pulse" : ""] that contains the following at [ADMIN_VERBOSEJMP(src)]:" + var/list/gases = air_contents.gases + var/danger = FALSE + for(var/gas_index in 1 to length(gases)) + var/list/gas_info = gases[gases[gas_index]] + var/list/meta = gas_info[GAS_META] + var/name = meta[META_GAS_NAME] + var/moles = gas_info[MOLES] + + output += "[name]: [moles] moles." + if(gas_index <= 5) //the first five gases added + admin_output += "[name]: [moles] moles." + else if(gas_index == 6) // anddd the warning + admin_output += "Too many gases to log. Check investigate log." + //if moles_visible is undefined, default to default visibility + if(meta[META_GAS_DANGER] && moles > (meta[META_GAS_MOLES_VISIBLE] || MOLES_GAS_VISIBLE)) + danger = TRUE + + if(danger) //sent to admin's chat if contains dangerous gases + message_admins(admin_output.Join("\n")) + var/logmsg = output.Join("\n") investigate_log(logmsg, INVESTIGATE_ATMOS) release_log += logmsg @@ -769,12 +707,13 @@ /// Ejects tank from canister, if any /obj/machinery/portable_atmospherics/canister/proc/eject_tank(mob/user, wire_pulsed = FALSE) - if(holding) - if(valve_open) - message_admins("[ADMIN_LOOKUPFLW(user)] removed [holding] from [src] with valve still open [wire_pulsed ? "via wire pulse" : ""] at [ADMIN_VERBOSEJMP(src)] releasing contents into the [span_boldannounce("air")].") - user.investigate_log("removed the [holding] [wire_pulsed ? "via wire pulse" : ""], leaving the valve open and transferring into the [span_boldannounce("air")].", INVESTIGATE_ATMOS) - replace_tank(user, FALSE) - return TRUE + if(!holding) + return FALSE + if(valve_open) + message_admins("[ADMIN_LOOKUPFLW(user)] removed [holding] from [src] with valve still open [wire_pulsed ? "via wire pulse" : ""] at [ADMIN_VERBOSEJMP(src)] releasing contents into the [span_boldannounce("air")].") + user.investigate_log("removed the [holding] [wire_pulsed ? "via wire pulse" : ""], leaving the valve open and transferring into the [span_boldannounce("air")].", INVESTIGATE_ATMOS) + replace_tank(user, FALSE) + return TRUE /// Turns hyper-noblium crystal reaction suppression in the canister on or off /obj/machinery/portable_atmospherics/canister/proc/toggle_reaction_suppression(mob/user, wire_pulsed = FALSE) diff --git a/tgstation.dme b/tgstation.dme index 65e8d694d2f1b..b425fbb14aa29 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -1812,7 +1812,6 @@ #include "code\datums\wires\airlock.dm" #include "code\datums\wires\apc.dm" #include "code\datums\wires\autolathe.dm" -#include "code\datums\wires\canister.dm" #include "code\datums\wires\conveyor.dm" #include "code\datums\wires\ecto_sniffer.dm" #include "code\datums\wires\emitter.dm" From f02e5bbbf15f3dc36553b6069ecdacb972dd53a6 Mon Sep 17 00:00:00 2001 From: orange man <61334995+comfyorange@users.noreply.github.com> Date: Thu, 28 Dec 2023 10:35:51 +1300 Subject: [PATCH 19/44] Automatic changelog for PR #80607 [ci skip] --- html/changelogs/AutoChangeLog-pr-80607.yml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 html/changelogs/AutoChangeLog-pr-80607.yml diff --git a/html/changelogs/AutoChangeLog-pr-80607.yml b/html/changelogs/AutoChangeLog-pr-80607.yml new file mode 100644 index 0000000000000..53f833bec70bc --- /dev/null +++ b/html/changelogs/AutoChangeLog-pr-80607.yml @@ -0,0 +1,4 @@ +author: "LemonInTheDark" +delete-after: True +changes: + - rscdel: "Removes the wires from canisters. It's a cool idea, but cheap controlled maxcaps are bad actually" \ No newline at end of file From b9ad7b651eaa4ae7b52a75d8f1f44725376a0aba Mon Sep 17 00:00:00 2001 From: Changelogs Date: Thu, 28 Dec 2023 00:20:23 +0000 Subject: [PATCH 20/44] Automatic changelog compile [ci skip] --- html/changelogs/AutoChangeLog-pr-80213.yml | 4 ---- html/changelogs/AutoChangeLog-pr-80277.yml | 5 ----- html/changelogs/AutoChangeLog-pr-80586.yml | 4 ---- html/changelogs/AutoChangeLog-pr-80587.yml | 5 ----- html/changelogs/AutoChangeLog-pr-80607.yml | 4 ---- html/changelogs/archive/2023-12.yml | 18 ++++++++++++++++++ 6 files changed, 18 insertions(+), 22 deletions(-) delete mode 100644 html/changelogs/AutoChangeLog-pr-80213.yml delete mode 100644 html/changelogs/AutoChangeLog-pr-80277.yml delete mode 100644 html/changelogs/AutoChangeLog-pr-80586.yml delete mode 100644 html/changelogs/AutoChangeLog-pr-80587.yml delete mode 100644 html/changelogs/AutoChangeLog-pr-80607.yml diff --git a/html/changelogs/AutoChangeLog-pr-80213.yml b/html/changelogs/AutoChangeLog-pr-80213.yml deleted file mode 100644 index feb4a5b6cfa21..0000000000000 --- a/html/changelogs/AutoChangeLog-pr-80213.yml +++ /dev/null @@ -1,4 +0,0 @@ -author: "Ghommie" -delete-after: True -changes: - - rscadd: "Added a \"postal workers strike\" negative station trait. In the case of holidays and sunday though, it'll be a \"postal system overtime\" instead." \ No newline at end of file diff --git a/html/changelogs/AutoChangeLog-pr-80277.yml b/html/changelogs/AutoChangeLog-pr-80277.yml deleted file mode 100644 index 9622cadecc108..0000000000000 --- a/html/changelogs/AutoChangeLog-pr-80277.yml +++ /dev/null @@ -1,5 +0,0 @@ -author: "Ben10Omintrix" -delete-after: True -changes: - - rscadd: "basic bots can now display their paths on huds" - - bugfix: "medbots can research healing again" \ No newline at end of file diff --git a/html/changelogs/AutoChangeLog-pr-80586.yml b/html/changelogs/AutoChangeLog-pr-80586.yml deleted file mode 100644 index 24ff9e3416eb8..0000000000000 --- a/html/changelogs/AutoChangeLog-pr-80586.yml +++ /dev/null @@ -1,4 +0,0 @@ -author: "SyncIt21" -delete-after: True -changes: - - bugfix: "mercury & lithium will no longer make you randomly move outside of cryotubes or in ground less turfs (space, water, lava etc)" \ No newline at end of file diff --git a/html/changelogs/AutoChangeLog-pr-80587.yml b/html/changelogs/AutoChangeLog-pr-80587.yml deleted file mode 100644 index a7ac80e338050..0000000000000 --- a/html/changelogs/AutoChangeLog-pr-80587.yml +++ /dev/null @@ -1,5 +0,0 @@ -author: "necromanceranne" -delete-after: True -changes: - - spellcheck: "Fixes a typo in chat message when starting a Death Knell with the Vorpal Scythe" - - admin: "Adds logging for the death knell, both when starts and when it is completed successfully." \ No newline at end of file diff --git a/html/changelogs/AutoChangeLog-pr-80607.yml b/html/changelogs/AutoChangeLog-pr-80607.yml deleted file mode 100644 index 53f833bec70bc..0000000000000 --- a/html/changelogs/AutoChangeLog-pr-80607.yml +++ /dev/null @@ -1,4 +0,0 @@ -author: "LemonInTheDark" -delete-after: True -changes: - - rscdel: "Removes the wires from canisters. It's a cool idea, but cheap controlled maxcaps are bad actually" \ No newline at end of file diff --git a/html/changelogs/archive/2023-12.yml b/html/changelogs/archive/2023-12.yml index d1aa3b3584bc8..16c8643b7e784 100644 --- a/html/changelogs/archive/2023-12.yml +++ b/html/changelogs/archive/2023-12.yml @@ -867,3 +867,21 @@ - bugfix: NTOS Messenger should search as you type now timothymtorres: - qol: Add UI screentips to spraycans +2023-12-28: + Ben10Omintrix: + - rscadd: basic bots can now display their paths on huds + - bugfix: medbots can research healing again + Ghommie: + - rscadd: Added a "postal workers strike" negative station trait. In the case of + holidays and sunday though, it'll be a "postal system overtime" instead. + LemonInTheDark: + - rscdel: Removes the wires from canisters. It's a cool idea, but cheap controlled + maxcaps are bad actually + SyncIt21: + - bugfix: mercury & lithium will no longer make you randomly move outside of cryotubes + or in ground less turfs (space, water, lava etc) + necromanceranne: + - spellcheck: Fixes a typo in chat message when starting a Death Knell with the + Vorpal Scythe + - admin: Adds logging for the death knell, both when starts and when it is completed + successfully. From 8e8d93072066a6f2f85330afbc6740ad2601a4b2 Mon Sep 17 00:00:00 2001 From: Arturlang <24881678+Arturlang@users.noreply.github.com> Date: Thu, 28 Dec 2023 03:32:20 +0200 Subject: [PATCH 21/44] Makes immerse use weakrefs (#80594) ## About The Pull Request Immerse was causing harddels due to it having references to mobs. Makes it use weakrefs for mobs instead. ## Why It's Good For The Game Immerse would cause harddels if a mob was deleted while it was in it's list. It could probably also happen if a turf was deleted too, but doing that here would be much harder. no CL since nothing playerfacing --- code/datums/elements/immerse.dm | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/code/datums/elements/immerse.dm b/code/datums/elements/immerse.dm index 900c096aa6538..d4171588c31d2 100644 --- a/code/datums/elements/immerse.dm +++ b/code/datums/elements/immerse.dm @@ -96,8 +96,8 @@ /datum/element/immerse/proc/stop_immersion(turf/source) SIGNAL_HANDLER UnregisterSignal(source, list(COMSIG_ATOM_ABSTRACT_ENTERED, COMSIG_ATOM_AFTER_SUCCESSFUL_INITIALIZED_ON, COMSIG_ATOM_ABSTRACT_EXITED)) - for(var/atom/movable/movable as anything in attached_turfs_and_movables[source]) - remove_from_element(source, movable) + for(var/datum/weakref/movable as anything in attached_turfs_and_movables[source]) + remove_from_element(source, movable.resolve()) attached_turfs_and_movables -= source /** @@ -122,7 +122,7 @@ try_immerse(movable, buckled) RegisterSignal(movable, COMSIG_QDELETING, PROC_REF(on_movable_qdel)) - LAZYADD(attached_turfs_and_movables[source], movable) + LAZYADD(attached_turfs_and_movables[source], WEAKREF(movable)) ADD_TRAIT(movable, TRAIT_IMMERSED, ELEMENT_TRAIT(src)) /datum/element/immerse/proc/on_movable_qdel(atom/movable/source) @@ -170,7 +170,7 @@ movable.vis_contents |= vis_overlay - LAZYSET(immersed_movables, movable, vis_overlay) + LAZYSET(immersed_movables, WEAKREF(movable), vis_overlay) ///Initializes and caches a new visual overlay given parameters such as width, height and whether it should appear fully underwater. /datum/element/immerse/proc/generate_vis_overlay(width, height, is_below_water) @@ -212,11 +212,11 @@ ///This proc removes the vis_overlay, the keep together trait and some signals from the movable. /datum/element/immerse/proc/remove_immerse_overlay(atom/movable/movable) - var/atom/movable/immerse_overlay/vis_overlay = LAZYACCESS(immersed_movables, movable) + var/atom/movable/immerse_overlay/vis_overlay = LAZYACCESS(immersed_movables, WEAKREF(movable)) if(!vis_overlay) return movable.vis_contents -= vis_overlay - LAZYREMOVE(immersed_movables, movable) + LAZYREMOVE(immersed_movables, WEAKREF(movable)) if(HAS_TRAIT(movable, TRAIT_UNIQUE_IMMERSE)) UnregisterSignal(movable, list(COMSIG_ATOM_SPIN_ANIMATION, COMSIG_LIVING_POST_UPDATE_TRANSFORM)) qdel(vis_overlay) @@ -298,8 +298,8 @@ if(!(exited.loc in attached_turfs_and_movables)) remove_from_element(source, exited) else - LAZYREMOVE(attached_turfs_and_movables[source], exited) - LAZYADD(attached_turfs_and_movables[exited.loc], exited) + LAZYREMOVE(attached_turfs_and_movables[source], WEAKREF(exited)) + LAZYADD(attached_turfs_and_movables[exited.loc], WEAKREF(exited)) ///Remove any signal, overlay, trait given to the movable and reference to it within the element. /datum/element/immerse/proc/remove_from_element(turf/source, atom/movable/movable) @@ -311,7 +311,7 @@ UnregisterSignal(movable, list(COMSIG_LIVING_SET_BUCKLED, COMSIG_QDELETING)) REMOVE_TRAIT(movable, TRAIT_IMMERSED, ELEMENT_TRAIT(src)) - LAZYREMOVE(attached_turfs_and_movables[source], movable) + LAZYREMOVE(attached_turfs_and_movables[source], WEAKREF(movable)) /// A band-aid to keep the (unique) visual overlay from scaling and rotating along with its owner. I'm sorry. /datum/element/immerse/proc/on_update_transform(mob/living/source, resize, new_lying_angle, is_opposite_angle) @@ -320,7 +320,7 @@ new_transform.Scale(1/source.current_size) new_transform.Turn(-new_lying_angle) - var/atom/movable/immerse_overlay/vis_overlay = immersed_movables[source] + var/atom/movable/immerse_overlay/vis_overlay = immersed_movables[WEAKREF(source)] if(is_opposite_angle) vis_overlay.transform = new_transform vis_overlay.adjust_living_overlay_offset(source) @@ -361,7 +361,7 @@ ///Spin the overlay in the opposite direction so it doesn't look like it's spinning at all. /datum/element/immerse/proc/on_spin_animation(atom/source, speed, loops, segments, segment) SIGNAL_HANDLER - var/atom/movable/immerse_overlay/vis_overlay = immersed_movables[source] + var/atom/movable/immerse_overlay/vis_overlay = immersed_movables[WEAKREF(source)] vis_overlay.do_spin_animation(speed, loops, segments, -segment) ///We need to make sure to remove hard refs from the element when deleted. From 31da337b686ff3cdaf061684522bf1836de731bf Mon Sep 17 00:00:00 2001 From: Bloop <13398309+vinylspiders@users.noreply.github.com> Date: Wed, 27 Dec 2023 20:56:22 -0500 Subject: [PATCH 22/44] Spellchecks let's -> lets (#80612) ## About The Pull Request This is a spellcheck pr ## Changelog :cl: spellcheck: changes some let's to lets /:cl: --- code/modules/antagonists/heretic/magic/void_phase.dm | 2 +- code/modules/mob/living/simple_animal/slime/slime.dm | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/code/modules/antagonists/heretic/magic/void_phase.dm b/code/modules/antagonists/heretic/magic/void_phase.dm index a3f16a2aeaf18..350ca0f29c100 100644 --- a/code/modules/antagonists/heretic/magic/void_phase.dm +++ b/code/modules/antagonists/heretic/magic/void_phase.dm @@ -1,6 +1,6 @@ /datum/action/cooldown/spell/pointed/void_phase name = "Void Phase" - desc = "Let's you blink to your pointed destination, causes 3x3 aoe damage bubble \ + desc = "Lets you blink to your pointed destination, causes 3x3 aoe damage bubble \ around your pointed destination and your current location. \ It has a minimum range of 3 tiles and a maximum range of 9 tiles." background_icon_state = "bg_heretic" diff --git a/code/modules/mob/living/simple_animal/slime/slime.dm b/code/modules/mob/living/simple_animal/slime/slime.dm index 6a03e3cb522c3..d9fbfe8dfb341 100644 --- a/code/modules/mob/living/simple_animal/slime/slime.dm +++ b/code/modules/mob/living/simple_animal/slime/slime.dm @@ -354,7 +354,7 @@ for(var/datum/surgery/operations as anything in surgeries) if(operations.next_step(user, modifiers)) return TRUE - if(istype(attacking_item, /obj/item/stack/sheet/mineral/plasma) && !stat) //Let's you feed slimes plasma. + if(istype(attacking_item, /obj/item/stack/sheet/mineral/plasma) && !stat) //Lets you feed slimes plasma. add_friendship(user, 1) to_chat(user, span_notice("You feed the slime the plasma. It chirps happily.")) var/obj/item/stack/sheet/mineral/plasma/sheet = attacking_item From ec22b1d2b3d6faedd6415946964cbffedb623336 Mon Sep 17 00:00:00 2001 From: orange man <61334995+comfyorange@users.noreply.github.com> Date: Thu, 28 Dec 2023 14:56:41 +1300 Subject: [PATCH 23/44] Automatic changelog for PR #80612 [ci skip] --- html/changelogs/AutoChangeLog-pr-80612.yml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 html/changelogs/AutoChangeLog-pr-80612.yml diff --git a/html/changelogs/AutoChangeLog-pr-80612.yml b/html/changelogs/AutoChangeLog-pr-80612.yml new file mode 100644 index 0000000000000..ba3924d539c3e --- /dev/null +++ b/html/changelogs/AutoChangeLog-pr-80612.yml @@ -0,0 +1,4 @@ +author: "vinylspiders" +delete-after: True +changes: + - spellcheck: "changes some let's to lets" \ No newline at end of file From e73d6f540b6cfff6829769c1f33afeb32c540c68 Mon Sep 17 00:00:00 2001 From: Mothblocks <35135081+Mothblocks@users.noreply.github.com> Date: Wed, 27 Dec 2023 20:41:39 -0800 Subject: [PATCH 24/44] Cache sprite data alongside spritesheets, fixing emotes and a whole lotta other missing images (#80601) --- code/modules/asset_cache/asset_list.dm | 32 +++++++++++++++++++++++--- 1 file changed, 29 insertions(+), 3 deletions(-) diff --git a/code/modules/asset_cache/asset_list.dm b/code/modules/asset_cache/asset_list.dm index 7e5785250a5b0..e4a00ffb8bf38 100644 --- a/code/modules/asset_cache/asset_list.dm +++ b/code/modules/asset_cache/asset_list.dm @@ -165,7 +165,7 @@ GLOBAL_LIST_EMPTY(asset_datums) if (isnull(should_refresh)) // `fexists` seems to always fail on static-time - should_refresh = !fexists("[ASSET_CROSS_ROUND_CACHE_DIRECTORY]/spritesheet.[name].css") + should_refresh = !fexists(css_cache_filename()) || !fexists(data_cache_filename()) return should_refresh @@ -296,8 +296,17 @@ GLOBAL_LIST_EMPTY(asset_datums) return out.Join("\n") +/datum/asset/spritesheet/proc/css_cache_filename() + return "[ASSET_CROSS_ROUND_CACHE_DIRECTORY]/spritesheet.[name].css" + +/datum/asset/spritesheet/proc/data_cache_filename() + return "[ASSET_CROSS_ROUND_CACHE_DIRECTORY]/spritesheet.[name].json" + /datum/asset/spritesheet/proc/read_from_cache() - var/replaced_css = file2text("[ASSET_CROSS_ROUND_CACHE_DIRECTORY]/spritesheet.[name].css") + return read_css_from_cache() && read_data_from_cache() + +/datum/asset/spritesheet/proc/read_css_from_cache() + var/replaced_css = file2text(css_cache_filename()) var/regex/find_background_urls = regex(@"background:url\('%(.+?)%'\)", "g") while (find_background_urls.Find(replaced_css)) @@ -319,6 +328,14 @@ GLOBAL_LIST_EMPTY(asset_datums) return TRUE +/datum/asset/spritesheet/proc/read_data_from_cache() + var/json = json_decode(file2text(data_cache_filename())) + + if (islist(json["sprites"])) + sprites = json["sprites"] + + return TRUE + /datum/asset/spritesheet/proc/send_from_cache(client/client) if (isnull(cached_spritesheets_needed)) stack_trace("cached_spritesheets_needed was null when sending assets from [type] from cache") @@ -334,6 +351,10 @@ GLOBAL_LIST_EMPTY(asset_datums) return SSassets.transport.get_asset_url(asset) /datum/asset/spritesheet/proc/write_to_cache() + write_css_to_cache() + write_data_to_cache() + +/datum/asset/spritesheet/proc/write_css_to_cache() for (var/size_id in sizes) fcopy(SSassets.cache["[name]_[size_id].png"].resource, "[ASSET_CROSS_ROUND_CACHE_DIRECTORY]/spritesheet.[name]_[size_id].png") @@ -341,7 +362,12 @@ GLOBAL_LIST_EMPTY(asset_datums) var/mock_css = generate_css() generating_cache = FALSE - rustg_file_write(mock_css, "[ASSET_CROSS_ROUND_CACHE_DIRECTORY]/spritesheet.[name].css") + rustg_file_write(mock_css, css_cache_filename()) + +/datum/asset/spritesheet/proc/write_data_to_cache() + rustg_file_write(json_encode(list( + "sprites" = sprites, + )), data_cache_filename()) /datum/asset/spritesheet/proc/get_cached_url_mappings() var/list/mappings = list() From 9eb7ba9c6caac9ff5215f4723dc9af89dbb93632 Mon Sep 17 00:00:00 2001 From: orange man <61334995+comfyorange@users.noreply.github.com> Date: Thu, 28 Dec 2023 17:42:00 +1300 Subject: [PATCH 25/44] Automatic changelog for PR #80601 [ci skip] --- html/changelogs/AutoChangeLog-pr-80601.yml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 html/changelogs/AutoChangeLog-pr-80601.yml diff --git a/html/changelogs/AutoChangeLog-pr-80601.yml b/html/changelogs/AutoChangeLog-pr-80601.yml new file mode 100644 index 0000000000000..ab7644fe8a7b7 --- /dev/null +++ b/html/changelogs/AutoChangeLog-pr-80601.yml @@ -0,0 +1,4 @@ +author: "Mothblocks" +delete-after: True +changes: + - bugfix: "Fixed a lot of missing/broken images, such as emojis, language icons, commendation hearts, icons in R&D menu, etc." \ No newline at end of file From 7426a7bb1e7dc91c6bb6664ce1c3e7ef897117b0 Mon Sep 17 00:00:00 2001 From: SyncIt21 <110812394+SyncIt21@users.noreply.github.com> Date: Thu, 28 Dec 2023 13:18:36 +0530 Subject: [PATCH 26/44] [NO GBP] Some reaction & chamber fixes (#80600) ## About The Pull Request - Fixes #80596 The UI opens when you remove/add a beaker to it - Fixes #80597 Calls `handle_reactions()` per step and updates UI thus reporting changes more often - Fixes https://github.com/tgstation/tgstation/issues/80597#issuecomment-1869693182. For instant reactions such as plastic we want to call `handle_reactions()` wherever possible (in this case whenever new reagent is added per equilibrium step) so that we can maximise product formed and start new reactions whenever possible - Fixes #80632 Same explanation as above since it now triggers all reactions more often ## Changelog :cl: fix: reaction chamber open its UI when inserting/removing beakers from it fix: reaction chamber triggers new reactions & updates UI more often during heating fix: instant and normal reactions now get triggered more often so for e.g. more plastic sheets from polymer reactions or more reactions occur when there are multiple reagents present /:cl: --- code/modules/reagents/chemistry/equilibrium.dm | 2 +- code/modules/reagents/chemistry/holder/holder.dm | 2 +- .../reagents/chemistry/machinery/chem_heater.dm | 10 +++++++--- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/code/modules/reagents/chemistry/equilibrium.dm b/code/modules/reagents/chemistry/equilibrium.dm index c9d6226930cbe..ffa8e0c32d482 100644 --- a/code/modules/reagents/chemistry/equilibrium.dm +++ b/code/modules/reagents/chemistry/equilibrium.dm @@ -341,7 +341,7 @@ var/total_step_added = 0 for(var/datum/reagent/product as anything in reaction.results) //create the products - step_add = holder.add_reagent(product, delta_chem_factor * reaction.results[product], null, cached_temp, purity, override_base_ph = TRUE, no_react = TRUE) + step_add = holder.add_reagent(product, delta_chem_factor * reaction.results[product], null, cached_temp, purity, override_base_ph = TRUE) if(!step_add) to_delete = TRUE return diff --git a/code/modules/reagents/chemistry/holder/holder.dm b/code/modules/reagents/chemistry/holder/holder.dm index 867245e9c2524..5872f5db1aeee 100644 --- a/code/modules/reagents/chemistry/holder/holder.dm +++ b/code/modules/reagents/chemistry/holder/holder.dm @@ -194,7 +194,7 @@ set_temperature(reagtemp) SEND_SIGNAL(src, COMSIG_REAGENTS_NEW_REAGENT, new_reagent, amount, reagtemp, data, no_react) - if(!no_react && !is_reacting) + if(!no_react) handle_reactions() return amount diff --git a/code/modules/reagents/chemistry/machinery/chem_heater.dm b/code/modules/reagents/chemistry/machinery/chem_heater.dm index f0113153c92d3..c31c099c2550c 100644 --- a/code/modules/reagents/chemistry/machinery/chem_heater.dm +++ b/code/modules/reagents/chemistry/machinery/chem_heater.dm @@ -146,12 +146,14 @@ //keep constant with the chemical acclimator please beaker.reagents.adjust_thermal_energy((target_temperature - beaker.reagents.chem_temp) * heater_coefficient * seconds_per_tick * SPECIFIC_HEAT_DEFAULT * beaker.reagents.total_volume * randomness) - if(!beaker.reagents.is_reacting) - beaker.reagents.handle_reactions() + beaker.reagents.handle_reactions() //use power use_power(active_power_usage * seconds_per_tick) + //show changes to ui immediatly for responsivenes + SStgui.update_uis(src) + /obj/machinery/chem_heater/wrench_act(mob/living/user, obj/item/tool) . = ITEM_INTERACT_BLOCKING if(default_unfasten_wrench(user, tool) == SUCCESSFUL_UNFASTEN) @@ -176,8 +178,10 @@ var/obj/item/reagent_containers/injector = held_item injector.afterattack(beaker, user, proximity_flag = TRUE) return TRUE + if(is_reagent_container(held_item) && held_item.is_open_container()) - replace_beaker(user, held_item) + if(replace_beaker(user, held_item)) + ui_interact(user) balloon_alert(user, "beaker added!") return TRUE From 40efb6627edaf34249f266d057b3e13f7e183880 Mon Sep 17 00:00:00 2001 From: orange man <61334995+comfyorange@users.noreply.github.com> Date: Thu, 28 Dec 2023 20:48:54 +1300 Subject: [PATCH 27/44] Automatic changelog for PR #80600 [ci skip] --- html/changelogs/AutoChangeLog-pr-80600.yml | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 html/changelogs/AutoChangeLog-pr-80600.yml diff --git a/html/changelogs/AutoChangeLog-pr-80600.yml b/html/changelogs/AutoChangeLog-pr-80600.yml new file mode 100644 index 0000000000000..ae331f1d10aaf --- /dev/null +++ b/html/changelogs/AutoChangeLog-pr-80600.yml @@ -0,0 +1,6 @@ +author: "SyncIt21" +delete-after: True +changes: + - bugfix: "reaction chamber open its UI when inserting/removing beakers from it" + - bugfix: "reaction chamber triggers new reactions & updates UI more often during heating" + - bugfix: "instant and normal reactions now get triggered more often so for e.g. more plastic sheets from polymer reactions or more reactions occur when there are multiple reagents present" \ No newline at end of file From 06f169993c78807e66a55c2c9f52b059e6bc73d1 Mon Sep 17 00:00:00 2001 From: LemonInTheDark <58055496+LemonInTheDark@users.noreply.github.com> Date: Thu, 28 Dec 2023 04:05:40 -0800 Subject: [PATCH 28/44] Fixes wallmounted lights falling on drag (#80609) ## About The Pull Request Being able to move around lights when using the light debugger is important Can't just be qdeling em whenever you try Closes https://github.com/tgstation/tgstation/issues/78662 ## Changelog :cl: fix: Dear mappers, the light debugger tool no longer deletes dragged wall lights /:cl: --- code/datums/components/wall_mounted.dm | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/code/datums/components/wall_mounted.dm b/code/datums/components/wall_mounted.dm index 00482592f2b3f..a10fb2468334d 100644 --- a/code/datums/components/wall_mounted.dm +++ b/code/datums/components/wall_mounted.dm @@ -19,7 +19,7 @@ ADD_TRAIT(parent, TRAIT_WALLMOUNTED, REF(src)) RegisterSignal(hanging_wall_turf, COMSIG_ATOM_EXAMINE, PROC_REF(on_examine)) RegisterSignal(hanging_wall_turf, COMSIG_TURF_CHANGE, PROC_REF(on_turf_changing)) - RegisterSignal(parent, COMSIG_MOVABLE_MOVED, PROC_REF(drop_wallmount)) + RegisterSignal(parent, COMSIG_MOVABLE_MOVED, PROC_REF(on_move)) RegisterSignal(parent, COMSIG_QDELETING, PROC_REF(on_linked_destroyed)) /datum/component/wall_mounted/UnregisterFromParent() @@ -51,6 +51,18 @@ if (ispath(path, /turf/open)) drop_wallmount() + +/** + * If we get dragged from our wall (by a singulo for instance) we should deconstruct + */ +/datum/component/wall_mounted/proc/on_move(datum/source, atom/old_loc, dir, forced, list/old_locs) + SIGNAL_HANDLER + // If we're having our lighting messed with we're likely to get dragged about + // That shouldn't lead to a decon + if(HAS_TRAIT(parent, TRAIT_LIGHTING_DEBUGGED)) + return + drop_wallmount() + /** * Handles the dropping of the linked object. This is done via deconstruction, as that should be the most sane way to handle it for most objects. * Except for intercoms, which are handled by creating a new wallframe intercom, as they're apparently items. @@ -68,6 +80,7 @@ if(!QDELING(src)) qdel(src) //Well, we fell off the wall, so we're done here. + /** * Checks object direction and then verifies if there's a wall in that direction. Finally, applies a wall_mounted component to the object. * From 23b9f4489f27977bcd9da5da857be1d48290ccfb Mon Sep 17 00:00:00 2001 From: orange man <61334995+comfyorange@users.noreply.github.com> Date: Fri, 29 Dec 2023 01:06:04 +1300 Subject: [PATCH 29/44] Automatic changelog for PR #80609 [ci skip] --- html/changelogs/AutoChangeLog-pr-80609.yml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 html/changelogs/AutoChangeLog-pr-80609.yml diff --git a/html/changelogs/AutoChangeLog-pr-80609.yml b/html/changelogs/AutoChangeLog-pr-80609.yml new file mode 100644 index 0000000000000..34fc5ff768221 --- /dev/null +++ b/html/changelogs/AutoChangeLog-pr-80609.yml @@ -0,0 +1,4 @@ +author: "LemonInTheDark" +delete-after: True +changes: + - bugfix: "Dear mappers, the light debugger tool no longer deletes dragged wall lights" \ No newline at end of file From b1c4974bb5cfa592f9d90deb4ec6c440682e2144 Mon Sep 17 00:00:00 2001 From: Jeremiah <42397676+jlsnow301@users.noreply.github.com> Date: Thu, 28 Dec 2023 04:51:37 -0800 Subject: [PATCH 30/44] Fixes input value not clearing on NTOS app [no gbp] (#80614) ## About The Pull Request Problem goes a little deeper than simply adding "selfClear" prop - ntos messenger is looking for more of a controlled component. Whenever messages are sent, it attempts to update the value in the input box ## Why It's Good For The Game Fixes #80611 ## Changelog :cl: fix: NTOS Messenger should clear on enter now /:cl: --- tgui/packages/tgui/components/Input.tsx | 29 +++++++++++++++++-------- 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/tgui/packages/tgui/components/Input.tsx b/tgui/packages/tgui/components/Input.tsx index c515627b8c12f..d9b517f1cb529 100644 --- a/tgui/packages/tgui/components/Input.tsx +++ b/tgui/packages/tgui/components/Input.tsx @@ -77,22 +77,33 @@ export const Input = (props: Props) => { } }; + /** Focuses the input on mount */ useEffect(() => { + if (!autoFocus && !autoSelect) return; + const input = inputRef.current; if (!input) return; - input.value = toInputValue(value); - if (autoFocus || autoSelect) { - setTimeout(() => { - input.focus(); + setTimeout(() => { + input.focus(); - if (autoSelect) { - input.select(); - } - }, 1); - } + if (autoSelect) { + input.select(); + } + }, 1); }, []); + /** Updates the initial value on props change */ + useEffect(() => { + const input = inputRef.current; + if (!input) return; + + const newValue = toInputValue(value); + if (input.value === newValue) return; + + input.value = newValue; + }, [value]); + return ( Date: Fri, 29 Dec 2023 01:51:55 +1300 Subject: [PATCH 31/44] Automatic changelog for PR #80614 [ci skip] --- html/changelogs/AutoChangeLog-pr-80614.yml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 html/changelogs/AutoChangeLog-pr-80614.yml diff --git a/html/changelogs/AutoChangeLog-pr-80614.yml b/html/changelogs/AutoChangeLog-pr-80614.yml new file mode 100644 index 0000000000000..cb847b2ceeec7 --- /dev/null +++ b/html/changelogs/AutoChangeLog-pr-80614.yml @@ -0,0 +1,4 @@ +author: "jlsnow301" +delete-after: True +changes: + - bugfix: "NTOS Messenger should clear on enter now" \ No newline at end of file From 712b0038e1225d145aca184c2b5f96e17abfa915 Mon Sep 17 00:00:00 2001 From: Mothblocks <35135081+Mothblocks@users.noreply.github.com> Date: Thu, 28 Dec 2023 04:54:36 -0800 Subject: [PATCH 32/44] Fix some incorrect uses of "src" in energized component (#80613) ## About The Pull Request Fixes this: ![image](https://github.com/tgstation/tgstation/assets/35135081/d4265136-9324-4057-992b-fbc9a6f6eb46) Untested ## Changelog :cl: fix: Fixed "was shocked by /datum/component/energized" message. /:cl: --- code/datums/components/energized.dm | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/code/datums/components/energized.dm b/code/datums/components/energized.dm index 97ff670113f99..d637bc2eb767a 100644 --- a/code/datums/components/energized.dm +++ b/code/datums/components/energized.dm @@ -76,7 +76,7 @@ if(prob(100 - toast_prob)) if(prob(25)) do_sparks(1, FALSE, source) - playsound(src, SFX_SPARKS, 40, TRUE, SHORT_RANGE_SOUND_EXTRARANGE) + playsound(parent, SFX_SPARKS, 40, TRUE, SHORT_RANGE_SOUND_EXTRARANGE) source.audible_message(span_danger("[parent] makes an electric crackle...")) return FALSE @@ -116,10 +116,10 @@ header = "Electrifying!", ) do_sparks(4, FALSE, source) - playsound(src, SFX_SPARKS, 75, TRUE, SHORT_RANGE_SOUND_EXTRARANGE) + playsound(parent, SFX_SPARKS, 75, TRUE, SHORT_RANGE_SOUND_EXTRARANGE) source.audible_message(span_danger("[parent] makes a loud electric crackle!")) to_chat(future_tram_victim, span_userdanger("You hear a loud electric crackle!")) - future_tram_victim.electrocute_act(15, src, 1) + future_tram_victim.electrocute_act(15, parent, 1) return TRUE #undef NORMAL_TOAST_PROB From a1b9c10924dece841bef28e62d09fb27c0f861cc Mon Sep 17 00:00:00 2001 From: orange man <61334995+comfyorange@users.noreply.github.com> Date: Fri, 29 Dec 2023 01:54:56 +1300 Subject: [PATCH 33/44] Automatic changelog for PR #80613 [ci skip] --- html/changelogs/AutoChangeLog-pr-80613.yml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 html/changelogs/AutoChangeLog-pr-80613.yml diff --git a/html/changelogs/AutoChangeLog-pr-80613.yml b/html/changelogs/AutoChangeLog-pr-80613.yml new file mode 100644 index 0000000000000..e5d82edfcecec --- /dev/null +++ b/html/changelogs/AutoChangeLog-pr-80613.yml @@ -0,0 +1,4 @@ +author: "Mothblocks" +delete-after: True +changes: + - bugfix: "Fixed \"was shocked by /datum/component/energized\" message." \ No newline at end of file From 404d2cb36cc325cbad126c275ab627ca086ed7c2 Mon Sep 17 00:00:00 2001 From: Mothblocks <35135081+Mothblocks@users.noreply.github.com> Date: Thu, 28 Dec 2023 05:14:49 -0800 Subject: [PATCH 34/44] Optimize find_potential_targets self cost (#80602) ![image](https://github.com/tgstation/tgstation/assets/35135081/84ae20b6-5f44-4a69-bda3-0df1435dea5c) `find_potential_targets/perform` currently has a pretty bad self cost in part due to it running a second "loop over everything in range" check to find turrets and mechs. This doesn't drop it down by as much as I'd like because it still needs `hearers`, it still shows up pretty high, but this at least cuts out some unnecessary work. Best case is likely to minimize work AIs need to do when there are no players on their z-level, as there are a lot of calls from Lavaland. --- .../ai/basic_mobs/basic_ai_behaviors/targeting.dm | 11 ++++++----- code/datums/elements/hostile_machine.dm | 15 +++++++++++++++ .../machinery/porta_turret/portable_turret.dm | 2 ++ code/modules/vehicles/mecha/_mecha.dm | 1 + tgstation.dme | 1 + 5 files changed, 25 insertions(+), 5 deletions(-) create mode 100644 code/datums/elements/hostile_machine.dm diff --git a/code/datums/ai/basic_mobs/basic_ai_behaviors/targeting.dm b/code/datums/ai/basic_mobs/basic_ai_behaviors/targeting.dm index 435b9cb1ef3d0..8ba9624c21edc 100644 --- a/code/datums/ai/basic_mobs/basic_ai_behaviors/targeting.dm +++ b/code/datums/ai/basic_mobs/basic_ai_behaviors/targeting.dm @@ -1,11 +1,12 @@ +/// List of objects that AIs will treat as targets +GLOBAL_LIST_EMPTY_TYPED(hostile_machines, /atom) + /datum/ai_behavior/find_potential_targets action_cooldown = 2 SECONDS /// How far can we see stuff? var/vision_range = 9 /// Blackboard key for aggro range, uses vision range if not specified var/aggro_range_key = BB_AGGRO_RANGE - /// Static typecache list of potentially dangerous objs - var/static/list/hostile_machines = typecacheof(list(/obj/machinery/porta_turret, /obj/vehicle/sealed/mecha)) /datum/ai_behavior/find_potential_targets/perform(seconds_per_tick, datum/ai_controller/controller, target_key, targeting_strategy_key, hiding_location_key) . = ..() @@ -26,9 +27,9 @@ var/list/potential_targets = hearers(aggro_range, get_turf(controller.pawn)) - living_mob //Remove self, so we don't suicide - for(var/HM in typecache_filter_list(range(aggro_range, living_mob), hostile_machines)) //Can we see any hostile machines? - if(can_see(living_mob, HM, aggro_range)) - potential_targets += HM + for (var/atom/hostile_machine as anything in GLOB.hostile_machines) + if (can_see(living_mob, hostile_machine, aggro_range)) + potential_targets += hostile_machine if(!potential_targets.len) finish_action(controller, succeeded = FALSE) diff --git a/code/datums/elements/hostile_machine.dm b/code/datums/elements/hostile_machine.dm new file mode 100644 index 0000000000000..0a5f19287bb24 --- /dev/null +++ b/code/datums/elements/hostile_machine.dm @@ -0,0 +1,15 @@ +/// AIs will attack this as a potential target if they see it +/datum/element/hostile_machine + element_flags = ELEMENT_DETACH_ON_HOST_DESTROY + +/datum/element/hostile_machine/Attach(datum/target) + . = ..() + + if (!isatom(target)) + return ELEMENT_INCOMPATIBLE + + GLOB.hostile_machines += target + +/datum/element/hostile_machine/Detach(datum/source) + GLOB.hostile_machines -= source + return ..() diff --git a/code/game/machinery/porta_turret/portable_turret.dm b/code/game/machinery/porta_turret/portable_turret.dm index 5a7e1da998424..76b1b73ba53f3 100644 --- a/code/game/machinery/porta_turret/portable_turret.dm +++ b/code/game/machinery/porta_turret/portable_turret.dm @@ -133,6 +133,8 @@ DEFINE_BITFIELD(turret_flags, list( if(!has_cover) INVOKE_ASYNC(src, PROC_REF(popUp)) + AddElement(/datum/element/hostile_machine) + /obj/machinery/porta_turret/proc/toggle_on(set_to) var/current = on if (!isnull(set_to)) diff --git a/code/modules/vehicles/mecha/_mecha.dm b/code/modules/vehicles/mecha/_mecha.dm index 208bd8d68c28d..d971b82a8566c 100644 --- a/code/modules/vehicles/mecha/_mecha.dm +++ b/code/modules/vehicles/mecha/_mecha.dm @@ -267,6 +267,7 @@ equip_by_category[key] -= path AddElement(/datum/element/falling_hazard, damage = 80, wound_bonus = 10, hardhat_safety = FALSE, crushes = TRUE) + AddElement(/datum/element/hostile_machine) /obj/vehicle/sealed/mecha/Destroy() for(var/ejectee in occupants) diff --git a/tgstation.dme b/tgstation.dme index b425fbb14aa29..672bc96d9f5c0 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -1403,6 +1403,7 @@ #include "code\datums\elements\haunted.dm" #include "code\datums\elements\high_fiver.dm" #include "code\datums\elements\honkspam.dm" +#include "code\datums\elements\hostile_machine.dm" #include "code\datums\elements\human_biter.dm" #include "code\datums\elements\immerse.dm" #include "code\datums\elements\item_fov.dm" From a3dc0d1a9391f6ece0786e18e4fa2920ad1a2112 Mon Sep 17 00:00:00 2001 From: MrMelbert <51863163+MrMelbert@users.noreply.github.com> Date: Thu, 28 Dec 2023 07:33:49 -0600 Subject: [PATCH 35/44] Deletes roundstart chemical reaction callback thing (#80618) ## About The Pull Request From what I can tell the only thing this was used for is not even necessary so why bother --- code/modules/reagents/chemistry/recipes.dm | 15 --------------- .../reagents/chemistry/recipes/pyrotechnics.dm | 15 ++++++++------- 2 files changed, 8 insertions(+), 22 deletions(-) diff --git a/code/modules/reagents/chemistry/recipes.dm b/code/modules/reagents/chemistry/recipes.dm index 26fb0f472ddc5..c7732e0908a0d 100644 --- a/code/modules/reagents/chemistry/recipes.dm +++ b/code/modules/reagents/chemistry/recipes.dm @@ -60,21 +60,6 @@ ///A bitflag var for tagging reagents for the reagent loopup functon var/reaction_tags = NONE -/datum/chemical_reaction/New() - . = ..() - SSticker.OnRoundstart(CALLBACK(src, PROC_REF(update_info))) - -/** - * Updates information during the roundstart - * - * This proc is mainly used by explosives but can be used anywhere else - * You should generally use the special reactions in [/datum/chemical_reaction/randomized] - * But for simple variable edits, like changing the temperature or adding/subtracting required reagents it is better to use this. - */ -/datum/chemical_reaction/proc/update_info() - return - - ///REACTION PROCS /** diff --git a/code/modules/reagents/chemistry/recipes/pyrotechnics.dm b/code/modules/reagents/chemistry/recipes/pyrotechnics.dm index 78942431bb8ba..8e74b0ad6f869 100644 --- a/code/modules/reagents/chemistry/recipes/pyrotechnics.dm +++ b/code/modules/reagents/chemistry/recipes/pyrotechnics.dm @@ -77,8 +77,9 @@ required_temp = 450 strengthdiv = 3 -/datum/chemical_reaction/reagent_explosion/tatp/update_info() - required_temp = 450 + rand(-49, 49) //this gets loaded only on round start +/datum/chemical_reaction/reagent_explosion/tatp/New() + . = ..() + required_temp = 450 + rand(-49, 49) /datum/chemical_reaction/reagent_explosion/tatp/on_reaction(datum/reagents/holder, datum/equilibrium/reaction, created_volume) if(holder.has_reagent(/datum/reagent/exotic_stabilizer, created_volume / 50)) // we like exotic stabilizer @@ -93,12 +94,12 @@ /datum/chemical_reaction/reagent_explosion/tatp_explosion/on_reaction(datum/reagents/holder, datum/equilibrium/reaction, created_volume) var/strengthdiv_adjust = created_volume / ( 2100 / initial(strengthdiv)) - strengthdiv = max(initial(strengthdiv) - strengthdiv_adjust + 1.5 ,1.5) //Slightly better than nitroglycerin - . = ..() - return + strengthdiv = max(initial(strengthdiv) - strengthdiv_adjust + 1.5, 1.5) //Slightly better than nitroglycerin + return ..() -/datum/chemical_reaction/reagent_explosion/tatp_explosion/update_info() - required_temp = 550 + rand(-49,49) +/datum/chemical_reaction/reagent_explosion/tatp_explosion/New() + . = ..() + required_temp = 550 + rand(-49, 49) /datum/chemical_reaction/reagent_explosion/penthrite_explosion_epinephrine required_reagents = list(/datum/reagent/medicine/c2/penthrite = 1, /datum/reagent/medicine/epinephrine = 1) From 527ebc345151129cee419c14cabec81a61ed1a4b Mon Sep 17 00:00:00 2001 From: SyncIt21 <110812394+SyncIt21@users.noreply.github.com> Date: Thu, 28 Dec 2023 19:28:22 +0530 Subject: [PATCH 36/44] Fixes for mat container & ORM (#80573) ## About The Pull Request - Fixes #80559 1) The ORM now hooks onto the local container only if off station. The ui act if statement was also messed up but that's fixed now too. 2) Creates a dedicated signal for items inserted into the silo for clarity & uses the helper proc defined inside remote materials for inserting items so we don't have to specify the `context` manually. 3) Properly updates the auto Doc for the container signal defines ## Changelog :cl: fix: Off station ORM's can redeem points again. /:cl: --- .../dcs/signals/signals_material_container.dm | 10 ++-- .../components/material/remote_materials.dm | 22 ++++++-- code/modules/mining/machine_redemption.dm | 50 +++++++++++++------ code/modules/mining/machine_silo.dm | 2 +- 4 files changed, 62 insertions(+), 22 deletions(-) diff --git a/code/__DEFINES/dcs/signals/signals_material_container.dm b/code/__DEFINES/dcs/signals/signals_material_container.dm index f33567a2739da..2c77be55c315d 100644 --- a/code/__DEFINES/dcs/signals/signals_material_container.dm +++ b/code/__DEFINES/dcs/signals/signals_material_container.dm @@ -2,10 +2,14 @@ /// Called from datum/component/material_container/proc/can_hold_material() : (mat) #define COMSIG_MATCONTAINER_MAT_CHECK "matcontainer_mat_check" #define MATCONTAINER_ALLOW_MAT (1<<0) -/// Called from datum/component/material_container/proc/user_insert() : (held_item, user) +/// Called from datum/component/material_container/proc/user_insert() : (target_item, user) #define COMSIG_MATCONTAINER_PRE_USER_INSERT "matcontainer_pre_user_insert" #define MATCONTAINER_BLOCK_INSERT (1<<1) -/// Called from datum/component/material_container/proc/insert_item() : (target, last_inserted_id, mats_consumed, material_amount, context) +/// Called from datum/component/material_container/proc/insert_item() : (item, primary_mat, mats_consumed, material_amount, context) #define COMSIG_MATCONTAINER_ITEM_CONSUMED "matcontainer_item_consumed" -/// Called from datum/component/material_container/proc/retrieve_sheets() : (sheets, context) +/// Called from datum/component/material_container/proc/retrieve_sheets() : (new_sheets, context) #define COMSIG_MATCONTAINER_SHEETS_RETRIEVED "matcontainer_sheets_retrieved" + +//mat container signals but from the ore silo's perspective +/// Called from /obj/machinery/ore_silo/on_item_consumed() : (container, item_inserted, last_inserted_id, mats_consumed, amount_inserted) +#define COMSIG_SILO_ITEM_CONSUMED "silo_item_consumed" diff --git a/code/datums/components/material/remote_materials.dm b/code/datums/components/material/remote_materials.dm index 376157903a6ce..33d85a5376f70 100644 --- a/code/datums/components/material/remote_materials.dm +++ b/code/datums/components/material/remote_materials.dm @@ -19,15 +19,24 @@ handles linking back and forth. var/allow_standalone ///Local size of container when silo = null var/local_size = INFINITY - ///Flags used when converting inserted materials into their component materials. + ///Flags used for the local material container(exceptions for item insert & intent flags) var/mat_container_flags = NONE - -/datum/component/remote_materials/Initialize(mapload, allow_standalone = TRUE, force_connect = FALSE, mat_container_flags = NONE) + ///List of signals to hook onto the local container + var/list/mat_container_signals + +/datum/component/remote_materials/Initialize( + mapload, + allow_standalone = TRUE, + force_connect = FALSE, + mat_container_flags = NONE, + list/mat_container_signals = null +) if (!isatom(parent)) return COMPONENT_INCOMPATIBLE src.allow_standalone = allow_standalone src.mat_container_flags = mat_container_flags + src.mat_container_signals = mat_container_signals RegisterSignal(parent, COMSIG_ATOM_TOOL_ACT(TOOL_MULTITOOL), PROC_REF(OnMultitool)) @@ -51,6 +60,8 @@ handles linking back and forth. * only if allow_standalone = TRUE, else you a null mat_container */ /datum/component/remote_materials/proc/_PrepareStorage(connect_to_silo) + PRIVATE_PROC(TRUE) + if (connect_to_silo) silo = GLOB.ore_silo_default if (silo) @@ -69,6 +80,8 @@ handles linking back and forth. return ..() /datum/component/remote_materials/proc/_MakeLocal() + PRIVATE_PROC(TRUE) + silo = null var/static/list/allowed_mats = list( @@ -90,6 +103,7 @@ handles linking back and forth. allowed_mats, \ local_size, \ mat_container_flags, \ + container_signals = mat_container_signals, \ allowed_items = /obj/item/stack \ ) @@ -268,7 +282,7 @@ handles linking back and forth. * * obj/item/weapon - the item you are trying to insert * * multiplier - the multiplier applied on the materials consumed */ -/datum/component/remote_materials/proc/insert_item(obj/item/weapon, multiplier) +/datum/component/remote_materials/proc/insert_item(obj/item/weapon, multiplier = 1) if(!_can_use_resource(FALSE)) return MATERIAL_INSERT_ITEM_FAILURE diff --git a/code/modules/mining/machine_redemption.dm b/code/modules/mining/machine_redemption.dm index 2d542445f8a6c..cfa8203615e7e 100644 --- a/code/modules/mining/machine_redemption.dm +++ b/code/modules/mining/machine_redemption.dm @@ -52,9 +52,21 @@ if(!GLOB.autounlock_techwebs[/datum/techweb/autounlocking/smelter]) GLOB.autounlock_techwebs[/datum/techweb/autounlocking/smelter] = new /datum/techweb/autounlocking/smelter stored_research = GLOB.autounlock_techwebs[/datum/techweb/autounlocking/smelter] - materials = AddComponent(/datum/component/remote_materials, mapload) - RegisterSignal(src, COMSIG_MATCONTAINER_ITEM_CONSUMED, TYPE_PROC_REF(/obj/machinery/mineral/ore_redemption, redeem_points)) + //mat_container_signals is for reedeming points from local storage if silo is not required + var/list/local_signals = null + if(!requires_silo) + local_signals = list( + COMSIG_MATCONTAINER_ITEM_CONSUMED = TYPE_PROC_REF(/obj/machinery/mineral/ore_redemption, local_redeem_points) + ) + materials = AddComponent( \ + /datum/component/remote_materials, \ + mapload, \ + mat_container_signals = local_signals \ + ) + + //for reedeming points from items inserted into ore silo + RegisterSignal(src, COMSIG_SILO_ITEM_CONSUMED, TYPE_PROC_REF(/obj/machinery/mineral/ore_redemption, silo_redeem_points)) /obj/machinery/mineral/ore_redemption/Destroy() stored_research = null @@ -67,7 +79,12 @@ . += span_notice("Alt-click to rotate the input and output direction.") -/obj/machinery/mineral/ore_redemption/proc/redeem_points(obj/machinery/mineral/ore_redemption/machine, container, obj/item/stack/ore/gathered_ore) +/obj/machinery/mineral/ore_redemption/proc/silo_redeem_points(obj/machinery/mineral/ore_redemption/machine, container, obj/item/stack/ore/gathered_ore) + SIGNAL_HANDLER + + local_redeem_points(container, gathered_ore) + +/obj/machinery/mineral/ore_redemption/proc/local_redeem_points(container, obj/item/stack/ore/gathered_ore) SIGNAL_HANDLER if(istype(gathered_ore) && gathered_ore.refined_type) @@ -156,7 +173,7 @@ if(isnull(gathered_ore.refined_type)) continue - if(materials.mat_container.insert_item(gathered_ore, ore_multiplier, context = src) <= 0) + if(materials.insert_item(gathered_ore, ore_multiplier) <= 0) unload_mineral(gathered_ore) //if rejected unload SEND_SIGNAL(src, COMSIG_ORM_COLLECTED_ORE) @@ -289,21 +306,26 @@ var/datum/component/material_container/mat_container = materials.mat_container switch(action) if("Claim") + //requires silo but silo not in range + if(requires_silo && !materials.check_z_level()) + return FALSE + + //no ID var/obj/item/card/id/user_id_card if(isliving(usr)) var/mob/living/user = usr user_id_card = user.get_idcard(TRUE) - if(!materials.check_z_level() && (requires_silo || !user_id_card.registered_account.replaceable)) - return TRUE + if(isnull(user_id_card)) + to_chat(usr, span_warning("No valid ID detected.")) + return FALSE + + //we have points if(points) - if(user_id_card) - user_id_card.registered_account.mining_points += points - points = 0 - else - to_chat(usr, span_warning("No valid ID detected.")) - else - to_chat(usr, span_warning("No points to claim.")) - return TRUE + user_id_card.registered_account.mining_points += points + points = 0 + return TRUE + + return FALSE if("Release") if(!mat_container) return diff --git a/code/modules/mining/machine_silo.dm b/code/modules/mining/machine_silo.dm index 61e340d3540ef..dcc85dc1c1cc5 100644 --- a/code/modules/mining/machine_silo.dm +++ b/code/modules/mining/machine_silo.dm @@ -64,7 +64,7 @@ GLOBAL_LIST_EMPTY(silo_access_logs) silo_log(context, "deposited", amount_inserted, item_inserted.name, mats_consumed) - SEND_SIGNAL(context, COMSIG_MATCONTAINER_ITEM_CONSUMED, container, item_inserted, last_inserted_id, mats_consumed, amount_inserted) + SEND_SIGNAL(context, COMSIG_SILO_ITEM_CONSUMED, container, item_inserted, last_inserted_id, mats_consumed, amount_inserted) /obj/machinery/ore_silo/proc/log_sheets_ejected(datum/component/material_container/container, obj/item/stack/sheet/sheets, atom/context) SIGNAL_HANDLER From c731f77b08d3b05b7b2fa01adbcf4f08a2986e4f Mon Sep 17 00:00:00 2001 From: orange man <61334995+comfyorange@users.noreply.github.com> Date: Fri, 29 Dec 2023 02:58:43 +1300 Subject: [PATCH 37/44] Automatic changelog for PR #80573 [ci skip] --- html/changelogs/AutoChangeLog-pr-80573.yml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 html/changelogs/AutoChangeLog-pr-80573.yml diff --git a/html/changelogs/AutoChangeLog-pr-80573.yml b/html/changelogs/AutoChangeLog-pr-80573.yml new file mode 100644 index 0000000000000..423125e03c911 --- /dev/null +++ b/html/changelogs/AutoChangeLog-pr-80573.yml @@ -0,0 +1,4 @@ +author: "SyncIt21" +delete-after: True +changes: + - bugfix: "Off station ORM's can redeem points again." \ No newline at end of file From 74375cb84fdc910fd60c856bcbea394c44380ec8 Mon Sep 17 00:00:00 2001 From: Tim Date: Thu, 28 Dec 2023 08:13:15 -0600 Subject: [PATCH 38/44] Add 50% graffiti speed boost to tagger quirk (#80567) ## About The Pull Request This gives the tagger quirk a 50% speed boost when drawing graffiti. There was also some unused variable intended to make large graffiti more time consuming that is now fixed. ## Why It's Good For The Game It'd be nice to be able to spraypaint messages on the floor quicker. Especially if you are doing a gimmick that requires making a shop sign using letters. ## Changelog :cl: add: Add 50% graffiti speed boost to tagger quirk fix: Fix time duration of large graffiti not applying properly /:cl: --- code/datums/quirks/positive_quirks/tagger.dm | 4 ++-- code/game/objects/items/crayons.dm | 8 ++++++-- code/game/objects/items/syndie_spraycan.dm | 10 +++++++++- 3 files changed, 17 insertions(+), 5 deletions(-) diff --git a/code/datums/quirks/positive_quirks/tagger.dm b/code/datums/quirks/positive_quirks/tagger.dm index 4b0f48a1ca893..c2640081a6d96 100644 --- a/code/datums/quirks/positive_quirks/tagger.dm +++ b/code/datums/quirks/positive_quirks/tagger.dm @@ -1,10 +1,10 @@ /datum/quirk/item_quirk/tagger name = "Tagger" - desc = "You're an experienced artist. People will actually be impressed by your graffiti, and you can get twice as many uses out of drawing supplies." + desc = "You're an experienced artist. People will actually be impressed by your graffiti, and you can get twice as many uses out of drawing supplies in half the time." icon = FA_ICON_SPRAY_CAN value = 4 mob_trait = TRAIT_TAGGER - gain_text = span_notice("You know how to tag walls efficiently.") + gain_text = span_notice("You know how to tag walls efficiently and quickly.") lose_text = span_danger("You forget how to tag walls properly.") medical_record_text = "Patient was recently seen for possible paint huffing incident." mail_goodies = list( diff --git a/code/game/objects/items/crayons.dm b/code/game/objects/items/crayons.dm index 35732e2310173..3090842b65ac8 100644 --- a/code/game/objects/items/crayons.dm +++ b/code/game/objects/items/crayons.dm @@ -14,6 +14,7 @@ #define AVAILABLE_SPRAYCAN_SPACE 8 // enough to fill one radial menu page +#define DRAW_TIME 5 SECONDS #define INFINITE_CHARGES -1 /* @@ -504,11 +505,13 @@ audible_message(span_notice("You hear spraying.")) playsound(user.loc, 'sound/effects/spray.ogg', 5, TRUE, 5) - var/wait_time = 50 + var/wait_time = DRAW_TIME if(paint_mode == PAINT_LARGE_HORIZONTAL) wait_time *= 3 + if(istagger) + wait_time *= 0.5 - if(!instant && !do_after(user, 50, target = target)) + if(!instant && !do_after(user, wait_time, target = target)) return if(!use_charges(user, cost)) @@ -1064,3 +1067,4 @@ #undef PAINT_LARGE_HORIZONTAL_ICON #undef INFINITE_CHARGES +#undef DRAW_TIME diff --git a/code/game/objects/items/syndie_spraycan.dm b/code/game/objects/items/syndie_spraycan.dm index deab6229a28fe..02a576a2cb97d 100644 --- a/code/game/objects/items/syndie_spraycan.dm +++ b/code/game/objects/items/syndie_spraycan.dm @@ -1,3 +1,5 @@ +#define SYNDIE_DRAW_TIME 3 SECONDS + // Extending the existing spraycan item was more trouble than it was worth, I don't want or need this to be able to draw arbitrary shapes. /obj/item/traitor_spraycan name = "seditious spraycan" @@ -79,7 +81,12 @@ /obj/item/traitor_spraycan/proc/try_draw_step(start_output, mob/living/user, atom/target) drawing_rune = TRUE user.balloon_alert(user, "[start_output]") - if (!do_after(user, 3 SECONDS, target)) + var/wait_time = SYNDIE_DRAW_TIME + + if(HAS_TRAIT(user, TRAIT_TAGGER)) + wait_time *= 0.5 + + if(!do_after(user, wait_time, target)) user.balloon_alert(user, "interrupted!") drawing_rune = FALSE return FALSE @@ -220,6 +227,7 @@ return ..() +#undef SYNDIE_DRAW_TIME #undef RUNE_STAGE_COLOURED #undef RUNE_STAGE_COMPLETE #undef RUNE_STAGE_OUTLINE From 06d884709d60d4498a603c52825679fc17552660 Mon Sep 17 00:00:00 2001 From: orange man <61334995+comfyorange@users.noreply.github.com> Date: Fri, 29 Dec 2023 03:13:33 +1300 Subject: [PATCH 39/44] Automatic changelog for PR #80567 [ci skip] --- html/changelogs/AutoChangeLog-pr-80567.yml | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 html/changelogs/AutoChangeLog-pr-80567.yml diff --git a/html/changelogs/AutoChangeLog-pr-80567.yml b/html/changelogs/AutoChangeLog-pr-80567.yml new file mode 100644 index 0000000000000..920d7749b54c9 --- /dev/null +++ b/html/changelogs/AutoChangeLog-pr-80567.yml @@ -0,0 +1,5 @@ +author: "timothymtorres" +delete-after: True +changes: + - rscadd: "Add 50% graffiti speed boost to tagger quirk" + - bugfix: "Fix time duration of large graffiti not applying properly" \ No newline at end of file From f2f859d729b8967fcc582364904d892458b7c721 Mon Sep 17 00:00:00 2001 From: Jordan Dominion Date: Thu, 28 Dec 2023 09:22:05 -0500 Subject: [PATCH 40/44] Update TGS Tester script for TGS6 (#80558) Fix broken CI's --- .github/workflows/tgs_test.yml | 4 ++-- tools/tgs_test/Program.cs | 11 ++++++++--- tools/tgs_test/Tgstation.TgsTest.csproj | 9 +++++---- 3 files changed, 15 insertions(+), 9 deletions(-) diff --git a/.github/workflows/tgs_test.yml b/.github/workflows/tgs_test.yml index 4925019bc6a02..6a9316f493a12 100644 --- a/.github/workflows/tgs_test.yml +++ b/.github/workflows/tgs_test.yml @@ -50,7 +50,7 @@ jobs: env: Database__DatabaseType: Sqlite Database__ConnectionString: Data Source=TGS_TGTest.sqlite3;Mode=ReadWriteCreate - General__ConfigVersion: 4.1.0 + General__ConfigVersion: 5.0.0 General__ApiPort: ${{ env.TGS_API_PORT }} General__SetupWizardMode: Never ports: @@ -59,7 +59,7 @@ jobs: - name: Setup dotnet uses: actions/setup-dotnet@v2 with: - dotnet-version: 7.0.x + dotnet-version: 8.0.x - name: Checkout Repository uses: actions/checkout@v3 diff --git a/tools/tgs_test/Program.cs b/tools/tgs_test/Program.cs index b8967dbc156da..9b018be951676 100644 --- a/tools/tgs_test/Program.cs +++ b/tools/tgs_test/Program.cs @@ -11,6 +11,7 @@ using Tgstation.Server.Api.Models; using Tgstation.Server.Api.Models.Response; using Tgstation.Server.Client; +using Tgstation.Server.Common.Extensions; using YamlDotNet.Serialization.NamingConventions; using YamlDotNet.Serialization; @@ -202,10 +203,14 @@ default); Console.WriteLine("Installing BYOND..."); - var byondInstallJob = await instanceClient.Byond.SetActiveVersion( - new ByondVersionRequest + var byondInstallJob = await instanceClient.Engine.SetActiveVersion( + new EngineVersionRequest { - Version = targetByondVersion + EngineVersion = new EngineVersion + { + Version = targetByondVersion, + Engine = EngineType.Byond, + } }, null, default); diff --git a/tools/tgs_test/Tgstation.TgsTest.csproj b/tools/tgs_test/Tgstation.TgsTest.csproj index 0a641c269ab2e..967fbd4295160 100644 --- a/tools/tgs_test/Tgstation.TgsTest.csproj +++ b/tools/tgs_test/Tgstation.TgsTest.csproj @@ -1,16 +1,17 @@ - 1.0.0 + 2.0.0 Exe - net7.0 + net8.0 enable enable - - + + + From 36956cf59d117e43ca64a8c3cfb119ff068e2697 Mon Sep 17 00:00:00 2001 From: Mothblocks <35135081+Mothblocks@users.noreply.github.com> Date: Thu, 28 Dec 2023 06:40:43 -0800 Subject: [PATCH 41/44] Add system for safely manipulating JSON databases and apply it to photo albums and photo frames (#80519) We frequently have issues with data loss in our long storage .json files for various reasons, such as the file being completely blanked out on write etc. This introduces a system that tries to safely handle that by saving the known working json file into a backup that will be loaded in the case a write fails. This system queues updates in order to send through to the next tick. This is an improvement over the existing implementation of photo albums and photo frames (I think all persistence, even) which do not save until the end of a properly rebooted round, but not during a server crash. Also saves the jsons in pretty prints, which make them easier to read but especially make them easier to diff in a git repository, which MSO wants to setup (and hopefully make public so I can make a dashboard on bus.moth.fans for looking at photo albums and their history, which is something I've wanted to do for a very long time). ## Changelog :cl: refactor: Photo albums and photo frames are now more resilient to data loss, especially when a server crashes. /:cl: --- .../subsystem/persistence/_persistence.dm | 20 ++- .../subsystem/persistence/photo_albums.dm | 79 ++--------- code/datums/json_database.dm | 128 ++++++++++++++++++ code/modules/photography/photos/album.dm | 36 +++-- code/modules/photography/photos/frame.dm | 38 +++--- tgstation.dme | 1 + 6 files changed, 207 insertions(+), 95 deletions(-) create mode 100644 code/datums/json_database.dm diff --git a/code/controllers/subsystem/persistence/_persistence.dm b/code/controllers/subsystem/persistence/_persistence.dm index 8d00c77812170..8f865e5d09871 100644 --- a/code/controllers/subsystem/persistence/_persistence.dm +++ b/code/controllers/subsystem/persistence/_persistence.dm @@ -20,8 +20,23 @@ SUBSYSTEM_DEF(persistence) var/list/blocked_maps = list() var/list/saved_trophies = list() var/list/picture_logging_information = list() - var/list/obj/structure/sign/picture_frame/photo_frames - var/list/obj/item/storage/photo_album/photo_albums + + /// A json_database linking to data/photo_frames.json. + /// Schema is persistence_id => array of photo names. + var/datum/json_database/photo_frames_database + + /// A lazy list of every picture frame that is going to be loaded with persistent photos. + /// Will be null'd once the persistence system initializes, and never read from again. + var/list/obj/structure/sign/picture_frame/queued_photo_frames + + /// A json_database linking to data/photo_albums.json. + /// Schema is persistence_id => array of photo names. + var/datum/json_database/photo_albums_database + + /// A lazy list of every photo album that is going to be loaded with persistent photos. + /// Will be null'd once the persistence system initializes, and never read from again. + var/list/obj/item/storage/photo_album/queued_photo_albums + var/rounds_since_engine_exploded = 0 var/delam_highscore = 0 var/tram_hits_this_round = 0 @@ -47,7 +62,6 @@ SUBSYSTEM_DEF(persistence) save_prisoner_tattoos() collect_trophies() collect_maps() - save_photo_persistence() //THIS IS PERSISTENCE, NOT THE LOGGING PORTION. save_randomized_recipes() save_scars() save_custom_outfits() diff --git a/code/controllers/subsystem/persistence/photo_albums.dm b/code/controllers/subsystem/persistence/photo_albums.dm index 3aee856b2c21c..90c8ec31ac146 100644 --- a/code/controllers/subsystem/persistence/photo_albums.dm +++ b/code/controllers/subsystem/persistence/photo_albums.dm @@ -1,15 +1,3 @@ -///Loads up the photo album source file. -/datum/controller/subsystem/persistence/proc/get_photo_albums() - var/album_path = file("data/photo_albums.json") - if(fexists(album_path)) - return json_decode(file2text(album_path)) - -///Loads up the photo frames source file. -/datum/controller/subsystem/persistence/proc/get_photo_frames() - var/frame_path = file("data/photo_frames.json") - if(fexists(frame_path)) - return json_decode(file2text(frame_path)) - /// Removes the identifier of a persistent photo frame from the json. /datum/controller/subsystem/persistence/proc/remove_photo_frames(identifier) var/frame_path = file("data/photo_frames.json") @@ -25,62 +13,23 @@ ///Loads photo albums, and populates them; also loads and applies frames to picture frames. /datum/controller/subsystem/persistence/proc/load_photo_persistence() - var/album_path = file("data/photo_albums.json") - var/frame_path = file("data/photo_frames.json") - if(fexists(album_path)) - var/list/json = json_decode(file2text(album_path)) - if(json.len) - for(var/i in photo_albums) - var/obj/item/storage/photo_album/A = i - if(!A.persistence_id) - continue - if(json[A.persistence_id]) - A.populate_from_id_list(json[A.persistence_id]) - - if(fexists(frame_path)) - var/list/json = json_decode(file2text(frame_path)) - if(json.len) - for(var/i in photo_frames) - var/obj/structure/sign/picture_frame/PF = i - if(!PF.persistence_id) - continue - if(json[PF.persistence_id]) - PF.load_from_id(json[PF.persistence_id]) - -///Saves the contents of photo albums and the picture frames. -/datum/controller/subsystem/persistence/proc/save_photo_persistence() - var/album_path = file("data/photo_albums.json") - var/frame_path = file("data/photo_frames.json") - - var/list/frame_json = list() - var/list/album_json = list() - - if(fexists(album_path)) - album_json = json_decode(file2text(album_path)) - fdel(album_path) - - for(var/i in photo_albums) - var/obj/item/storage/photo_album/A = i - if(!istype(A) || !A.persistence_id) + photo_albums_database = new("data/photo_albums.json") + for (var/obj/item/storage/photo_album/album as anything in queued_photo_albums) + if (isnull(album.persistence_id)) continue - var/list/L = A.get_picture_id_list() - album_json[A.persistence_id] = L - - album_json = json_encode(album_json) - - WRITE_FILE(album_path, album_json) - if(fexists(frame_path)) - frame_json = json_decode(file2text(frame_path)) - fdel(frame_path) + var/album_data = photo_albums_database.get_key(album.persistence_id) + if (!isnull(album_data)) + album.populate_from_id_list(album_data) - for(var/i in photo_frames) - var/obj/structure/sign/picture_frame/F = i - if(!istype(F) || !F.persistence_id) + photo_frames_database = new("data/photo_frames.json") + for (var/obj/structure/sign/picture_frame/frame as anything in queued_photo_frames) + if (isnull(frame.persistence_id)) continue - frame_json[F.persistence_id] = F.get_photo_id() - frame_json = json_encode(frame_json) - - WRITE_FILE(frame_path, frame_json) + var/frame_data = photo_frames_database.get_key(frame.persistence_id) + if (!isnull(frame_data)) + frame.load_from_id(frame_data) + queued_photo_albums = null + queued_photo_frames = null diff --git a/code/datums/json_database.dm b/code/datums/json_database.dm new file mode 100644 index 0000000000000..ea3ff354b48ce --- /dev/null +++ b/code/datums/json_database.dm @@ -0,0 +1,128 @@ +/// Represents a json file being used as a database in the data/ folder. +/// Changes made here will save back to the associated file, with recovery. +/// Will defer writes until later if multiple happen in the same tick. +/// Do not add an extra cache on top of this. This IS your cache. +/datum/json_database + VAR_PRIVATE + filepath + backup_filepath + + cached_data + save_queued = FALSE + + static/existing_json_database = list() + +/datum/json_database/New(filepath) + if (IsAdminAdvancedProcCall()) + to_chat(usr, "json_database creation, linking to [html_encode(filepath)], was blocked.", confidential = TRUE) + return + + ASSERT(isnull(existing_json_database[filepath]), "[filepath] already has an associated json_database. You must expose it somehow and use that instead of making a new one.") + + existing_json_database[filepath] = TRUE + + src.filepath = filepath + backup_filepath = "[filepath].savebac" + + if (fexists(filepath)) + cached_data = safe_json_decode(file2text(filepath)) + if (isnull(cached_data)) + var/scenario = "[filepath] existed, but did not have valid JSON" + + if (fexists(backup_filepath)) + load_backup(scenario) + else + stack_trace("[scenario]. No backup could be found.") + cached_data = list() + else + if (fexists(backup_filepath)) + load_backup("[filepath] didn't exist") + else + cached_data = list() + +/datum/json_database/Destroy() + if (save_queued) + save() + + existing_json_database -= filepath + + return ..() + +/// Returns the cached data. +/// Be careful on holding onto this data for too long, as it can mutate when other stuff changes it. +/// Do not mutate it yourself. +/datum/json_database/proc/get() + return cached_data + +/// Returns the data with the given key. +/// For arrays, this is a number. +/// Be careful on holding onto this data for too long, as it can mutate when other stuff changes it. +/// Do not mutate it yourself. +/datum/json_database/proc/get_key(key) + return cached_data[key] + +/// Sets the data at the key to the value, and queues a save. +/datum/json_database/proc/set_key(key, value) + cached_data[key] = value + queue_save() + +/// Removes the data at the given item, and queues a save. +/// For dictionaries, this can be the key. +/// For arrays, this can be the value. +/datum/json_database/proc/remove(item) + UNTYPED_LIST_REMOVE(cached_data, item) + queue_save() + +/// Inserts the data at the end of what is assumed to be an array, and queues a save. +/datum/json_database/proc/insert(value) + UNTYPED_LIST_ADD(cached_data, value) + queue_save() + +/// Replaces the cache with the new data completely, and queues a save. +/// Do not touch the new data after passing it in. +/datum/json_database/proc/replace(list/new_data) + cached_data = new_data + queue_save() + +/datum/json_database/proc/queue_save() + PRIVATE_PROC(TRUE) + + if (save_queued) + return + + addtimer(CALLBACK(src, PROC_REF(save)), 0) + +/datum/json_database/proc/save() + PRIVATE_PROC(TRUE) + + save_queued = FALSE + + if (fexists(filepath)) + rustg_file_write(file2text(filepath), backup_filepath) + + rustg_file_write(json_encode(cached_data, JSON_PRETTY_PRINT), filepath) + + ASSERT(!isnull(safe_json_decode(file2text(filepath))), "JSON written to [filepath] was not valid. Backup will be preserved.") + + fdel(backup_filepath) + +/datum/json_database/proc/load_backup(scenario) + PRIVATE_PROC(TRUE) + + var/cached_contents = file2text(backup_filepath) + var/list/backed_up_data = safe_json_decode(cached_contents) + + if (isnull(backed_up_data)) + stack_trace("[scenario]. Backup existed, but also did not have valid JSON.") + cached_data = list() + else + stack_trace("[scenario]. Backup existed and was used instead. The JSON file has been updated.") + cached_data = backed_up_data + rustg_file_write(cached_contents, filepath) + +/datum/json_database/vv_edit_var(var_name, var_value) + switch (var_name) + if (nameof(filepath), nameof(backup_filepath)) + return FALSE + else + return ..() diff --git a/code/modules/photography/photos/album.dm b/code/modules/photography/photos/album.dm index 35d7f27017cdb..f8f8a8fb0625f 100644 --- a/code/modules/photography/photos/album.dm +++ b/code/modules/photography/photos/album.dm @@ -9,6 +9,7 @@ inhand_icon_state = "album" lefthand_file = 'icons/mob/inhands/items/books_lefthand.dmi' righthand_file = 'icons/mob/inhands/items/books_righthand.dmi' + storage_type = /datum/storage/photo_album resistance_flags = FLAMMABLE w_class = WEIGHT_CLASS_SMALL flags_1 = PREVENT_CONTENTS_EXPLOSION_1 @@ -16,13 +17,11 @@ /obj/item/storage/photo_album/Initialize(mapload) . = ..() - atom_storage.set_holdable(list(/obj/item/photo)) - atom_storage.max_total_storage = 42 - atom_storage.max_slots = 21 - LAZYADD(SSpersistence.photo_albums, src) + if (!SSpersistence.initialized) + LAZYADD(SSpersistence.queued_photo_albums, src) /obj/item/storage/photo_album/Destroy() - LAZYREMOVE(SSpersistence.photo_albums, src) + LAZYREMOVE(SSpersistence.queued_photo_albums, src) return ..() /obj/item/storage/photo_album/proc/get_picture_id_list() @@ -41,9 +40,9 @@ //Manual loading, DO NOT USE FOR HARDCODED/MAPPED IN ALBUMS. This is for if an album needs to be loaded mid-round from an ID. /obj/item/storage/photo_album/proc/persistence_load() - var/list/data = SSpersistence.get_photo_albums() - if(data[persistence_id]) - populate_from_id_list(data[persistence_id]) + var/list/data = SSpersistence.photo_albums_database.get_key(persistence_id) + if (!isnull(data)) + populate_from_id_list(data) /obj/item/storage/photo_album/proc/populate_from_id_list(list/ids) var/list/current_ids = get_picture_id_list() @@ -55,6 +54,27 @@ if(!atom_storage?.attempt_insert(P, override = TRUE)) qdel(P) +/datum/storage/photo_album + max_total_storage = 42 + max_slots = 21 + +/datum/storage/photo_album/New(atom/parent, max_slots, max_specific_storage, max_total_storage, numerical_stacking, allow_quick_gather, allow_quick_empty, collection_mode, attack_hand_interact) + . = ..() + set_holdable(list(/obj/item/photo)) + +/datum/storage/photo_album/proc/save_everything() + var/obj/item/storage/photo_album/album = parent.resolve() + ASSERT(istype(album)) + SSpersistence.photo_albums_database.set_key(album.persistence_id, album.get_picture_id_list()) + +/datum/storage/photo_album/handle_enter(datum/source, obj/item/arrived) + . = ..() + save_everything() + +/datum/storage/photo_album/handle_exit(datum/source, obj/item/gone) + . = ..() + save_everything() + /obj/item/storage/photo_album/hos name = "photo album (Head of Security)" icon_state = "album_blue" diff --git a/code/modules/photography/photos/frame.dm b/code/modules/photography/photos/frame.dm index f2d77553ea16a..4fbe3e034d88c 100644 --- a/code/modules/photography/photos/frame.dm +++ b/code/modules/photography/photos/frame.dm @@ -56,7 +56,7 @@ var/obj/structure/sign/picture_frame/PF = O PF.copy_overlays(src) if(displayed) - PF.framed = displayed + PF.set_and_save_framed(displayed) if(contents.len) var/obj/item/I = pick(contents) I.forceMove(PF) @@ -70,27 +70,19 @@ resistance_flags = FLAMMABLE var/obj/item/photo/framed var/persistence_id - var/del_id_on_destroy = FALSE var/art_value = OK_ART var/can_decon = TRUE -#define FRAME_DEFINE(id) /obj/structure/sign/picture_frame/##id/persistence_id = #id - -//Put default persistent frame defines here! - -#undef FRAME_DEFINE - /obj/structure/sign/picture_frame/Initialize(mapload, dir, building) . = ..() AddElement(/datum/element/art, art_value) - LAZYADD(SSpersistence.photo_frames, src) + if (!SSpersistence.initialized) + LAZYADD(SSpersistence.queued_photo_frames, src) if(dir) setDir(dir) /obj/structure/sign/picture_frame/Destroy() - LAZYREMOVE(SSpersistence.photo_frames, src) - if(persistence_id && del_id_on_destroy) - SSpersistence.remove_photo_frames(persistence_id) + LAZYREMOVE(SSpersistence.queued_photo_frames, src) return ..() /obj/structure/sign/picture_frame/proc/get_photo_id() @@ -99,9 +91,9 @@ //Manual loading, DO NOT USE FOR HARDCODED/MAPPED IN ALBUMS. This is for if an album needs to be loaded mid-round from an ID. /obj/structure/sign/picture_frame/proc/persistence_load() - var/list/data = SSpersistence.get_photo_frames() - if(data[persistence_id]) - load_from_id(data[persistence_id]) + var/list/data = SSpersistence.photo_frames_database.get_key(persistence_id) + if(!isnull(data)) + load_from_id(data) /obj/structure/sign/picture_frame/proc/load_from_id(id) var/obj/item/photo/old/P = load_photo_from_disk(id) @@ -113,6 +105,15 @@ framed = P update_appearance() +/// Given a photo (or null), will change the contained picture, and queue a persistent save. +/obj/structure/sign/picture_frame/proc/set_and_save_framed(obj/item/photo/photo) + framed = photo + + if (isnull(persistence_id)) + return + + SSpersistence.photo_frames_database.set_key(persistence_id, photo?.picture?.id) + /obj/structure/sign/picture_frame/examine(mob/user) . = ..() if(in_range(src, user)) @@ -141,7 +142,7 @@ tool.play_tool_sound(src) framed.forceMove(drop_location()) user.visible_message(span_warning("[user] cuts away [framed] from [src]!")) - framed = null + set_and_save_framed(null) update_appearance() return ITEM_INTERACT_SUCCESS @@ -155,7 +156,7 @@ var/obj/item/photo/P = I if(!user.transferItemToLoc(P, src)) return - framed = P + set_and_save_framed(P) update_appearance() return TRUE ..() @@ -177,7 +178,7 @@ var/obj/item/wallframe/picture/F = new /obj/item/wallframe/picture(loc) if(framed) F.displayed = framed - framed = null + set_and_save_framed(null) if(contents.len) var/obj/item/I = pick(contents) I.forceMove(F) @@ -277,7 +278,6 @@ /obj/structure/sign/picture_frame/portrait/bar persistence_id = "frame_bar" - del_id_on_destroy = TRUE ///Generates a persistence id unique to the current map. Every bar should feel a little bit different after all. /obj/structure/sign/picture_frame/portrait/bar/Initialize(mapload) diff --git a/tgstation.dme b/tgstation.dme index 672bc96d9f5c0..d1f902cf25de2 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -764,6 +764,7 @@ #include "code\datums\hotkeys_help.dm" #include "code\datums\http.dm" #include "code\datums\hud.dm" +#include "code\datums\json_database.dm" #include "code\datums\json_savefile.dm" #include "code\datums\lazy_template.dm" #include "code\datums\map_config.dm" From 0398e15a20bc7feab6cc088459e5dfb848d20f04 Mon Sep 17 00:00:00 2001 From: orange man <61334995+comfyorange@users.noreply.github.com> Date: Fri, 29 Dec 2023 03:41:02 +1300 Subject: [PATCH 42/44] Automatic changelog for PR #80519 [ci skip] --- html/changelogs/AutoChangeLog-pr-80519.yml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 html/changelogs/AutoChangeLog-pr-80519.yml diff --git a/html/changelogs/AutoChangeLog-pr-80519.yml b/html/changelogs/AutoChangeLog-pr-80519.yml new file mode 100644 index 0000000000000..7c94d9b4fc812 --- /dev/null +++ b/html/changelogs/AutoChangeLog-pr-80519.yml @@ -0,0 +1,4 @@ +author: "Mothblocks" +delete-after: True +changes: + - refactor: "Photo albums and photo frames are now more resilient to data loss, especially when a server crashes." \ No newline at end of file From 37cd61c0fac0b24f3ffb9bad8b067f2922479b26 Mon Sep 17 00:00:00 2001 From: Diamond_ Date: Thu, 28 Dec 2023 06:43:19 -0800 Subject: [PATCH 43/44] Fixes AI behavior with breaking cuffs and resisting aggressive grabs (#80328) ## About The Pull Request Makes it so that ai's recognize they're in a do_after after resisting, preventing them from processing and interrupting the do_after while they're in a condition that necessitates it. If it gets interrupted then they'll process as normal, or if they finish they'll be free. ## Why It's Good For The Game Monkeys can now resist things that they should've been, such as aggressive grabs or cuffs, instead of having their ai completely freeze when they're cuffed. ## Changelog :cl: fix: ai can now tell if it is in a do_after for resisting and will not interrupt it. monkeys also now don't freeze up when aggressively grabbed and will resist out of those and cuffs. /:cl: --- code/__DEFINES/ai/monkey.dm | 1 + code/datums/ai/_ai_controller.dm | 1 + code/datums/ai/generic/generic_behaviors.dm | 1 + code/datums/ai/monkey/monkey_controller.dm | 15 ++++++++++++++- 4 files changed, 17 insertions(+), 1 deletion(-) diff --git a/code/__DEFINES/ai/monkey.dm b/code/__DEFINES/ai/monkey.dm index fc4af74d9f080..0e8c44e40fa83 100644 --- a/code/__DEFINES/ai/monkey.dm +++ b/code/__DEFINES/ai/monkey.dm @@ -15,3 +15,4 @@ #define BB_MONKEY_TARGET_MONKEYS "BB_monkey_target_monkeys" #define BB_MONKEY_DISPOSING "BB_monkey_disposing" #define BB_MONKEY_RECRUIT_COOLDOWN "BB_monkey_recruit_cooldown" +#define BB_RESISTING "BB_resisting" diff --git a/code/datums/ai/_ai_controller.dm b/code/datums/ai/_ai_controller.dm index 6bbdabfc50dfe..04dc7901c857f 100644 --- a/code/datums/ai/_ai_controller.dm +++ b/code/datums/ai/_ai_controller.dm @@ -175,6 +175,7 @@ multiple modular subtrees with behaviors ///Runs any actions that are currently running /datum/ai_controller/process(seconds_per_tick) + if(!able_to_run()) SSmove_manager.stop_looping(pawn) //stop moving return //this should remove them from processing in the future through event-based stuff. diff --git a/code/datums/ai/generic/generic_behaviors.dm b/code/datums/ai/generic/generic_behaviors.dm index 4f61a26165945..43e37f66e8c8a 100644 --- a/code/datums/ai/generic/generic_behaviors.dm +++ b/code/datums/ai/generic/generic_behaviors.dm @@ -2,6 +2,7 @@ /datum/ai_behavior/resist/perform(seconds_per_tick, datum/ai_controller/controller) . = ..() var/mob/living/living_pawn = controller.pawn + living_pawn.ai_controller.set_blackboard_key(BB_RESISTING, TRUE) living_pawn.execute_resist() finish_action(controller, TRUE) diff --git a/code/datums/ai/monkey/monkey_controller.dm b/code/datums/ai/monkey/monkey_controller.dm index 781bfb70a85e5..8be754f8d4b57 100644 --- a/code/datums/ai/monkey/monkey_controller.dm +++ b/code/datums/ai/monkey/monkey_controller.dm @@ -26,9 +26,22 @@ have ways of interacting with a specific mob and control it. BB_MONKEY_GUN_NEURONS_ACTIVATED = FALSE, BB_MONKEY_GUN_WORKED = TRUE, BB_SONG_LINES = MONKEY_SONG, + BB_RESISTING = FALSE, ) idle_behavior = /datum/idle_behavior/idle_monkey +/datum/ai_controller/monkey/process(seconds_per_tick) + + var/mob/living/living_pawn = src.pawn + + if(!length(living_pawn.do_afters) && living_pawn.ai_controller.blackboard[BB_RESISTING]) + living_pawn.ai_controller.set_blackboard_key(BB_RESISTING, FALSE) + + if(living_pawn.ai_controller.blackboard[BB_RESISTING]) + return + + . = ..() + /datum/ai_controller/monkey/New(atom/new_pawn) var/static/list/control_examine = list( ORGAN_SLOT_EYES = span_monkey("eyes have a primal look in them."), @@ -91,7 +104,7 @@ have ways of interacting with a specific mob and control it. /datum/ai_controller/monkey/able_to_run() var/mob/living/living_pawn = pawn - if(IS_DEAD_OR_INCAP(living_pawn)) + if(living_pawn.incapacitated(IGNORE_RESTRAINTS | IGNORE_GRAB | IGNORE_STASIS) || living_pawn.stat > CONSCIOUS) return FALSE return ..() From 9b3b026a7590372e42ecb9c44283c1ea17bba506 Mon Sep 17 00:00:00 2001 From: orange man <61334995+comfyorange@users.noreply.github.com> Date: Fri, 29 Dec 2023 03:43:44 +1300 Subject: [PATCH 44/44] Automatic changelog for PR #80328 [ci skip] --- html/changelogs/AutoChangeLog-pr-80328.yml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 html/changelogs/AutoChangeLog-pr-80328.yml diff --git a/html/changelogs/AutoChangeLog-pr-80328.yml b/html/changelogs/AutoChangeLog-pr-80328.yml new file mode 100644 index 0000000000000..8bbebbd7078eb --- /dev/null +++ b/html/changelogs/AutoChangeLog-pr-80328.yml @@ -0,0 +1,4 @@ +author: "Diamond-74" +delete-after: True +changes: + - bugfix: "ai can now tell if it is in a do_after for resisting and will not interrupt it. monkeys also now don't freeze up when aggressively grabbed and will resist out of those and cuffs." \ No newline at end of file