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/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/__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/__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/__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/__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/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/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/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/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/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 ..()
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/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
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/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.
*
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/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.
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/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/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/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/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/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/crayons.dm b/code/game/objects/items/crayons.dm
index 5ba709ef324ba..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))
@@ -762,6 +765,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))
@@ -1040,3 +1067,4 @@
#undef PAINT_LARGE_HORIZONTAL_ICON
#undef INFINITE_CHARGES
+#undef DRAW_TIME
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/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
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/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/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()
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/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/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)
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
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)
. = ..()
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
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/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()
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
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"))
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)
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(
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/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
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
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-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
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
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
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
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
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
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
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
diff --git a/html/changelogs/archive/2023-12.yml b/html/changelogs/archive/2023-12.yml
index b2cc28dc011f4..16c8643b7e784 100644
--- a/html/changelogs/archive/2023-12.yml
+++ b/html/changelogs/archive/2023-12.yml
@@ -854,3 +854,34 @@
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
+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.
diff --git a/icons/obj/storage/crates.dmi b/icons/obj/storage/crates.dmi
index 274af66c820a4..9bc8f4d2c27e9 100644
Binary files a/icons/obj/storage/crates.dmi and b/icons/obj/storage/crates.dmi differ
diff --git a/tgstation.dme b/tgstation.dme
index d22f46afb66ca..d16555c199341 100644
--- a/tgstation.dme
+++ b/tgstation.dme
@@ -765,6 +765,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"
@@ -1404,6 +1405,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"
@@ -1813,7 +1815,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"
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 (
{
width="220px"
placeholder="Search by name or job..."
value={searchUser}
- onChange={(_, value) => setSearchUser(value)}
+ onInput={(_, value) => setSearchUser(value)}
/>
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
-
-
+
+
+