diff --git a/aurorastation.dme b/aurorastation.dme
index 293ca85ea81..26aedd24cbf 100644
--- a/aurorastation.dme
+++ b/aurorastation.dme
@@ -344,7 +344,9 @@
#include "code\controllers\subsystems\timer.dm"
#include "code\controllers\subsystems\trade.dm"
#include "code\controllers\subsystems\virtual_reality.dm"
+#include "code\controllers\subsystems\vis_contents.dm"
#include "code\controllers\subsystems\vote.dm"
+#include "code\controllers\subsystems\weather.dm"
#include "code\controllers\subsystems\zcopy.dm"
#include "code\controllers\subsystems\evacuation\evacuation.dm"
#include "code\controllers\subsystems\evacuation\evacuation_eta.dm"
@@ -403,6 +405,7 @@
#include "code\datums\records.dm"
#include "code\datums\ruins.dm"
#include "code\datums\sound_player.dm"
+#include "code\datums\state_machine.dm"
#include "code\datums\statistic.dm"
#include "code\datums\tgs_event_handler.dm"
#include "code\datums\tgui_module.dm"
@@ -480,6 +483,8 @@
#include "code\datums\repositories\singletons.dm"
#include "code\datums\repositories\sound_channels.dm"
#include "code\datums\repositories\unique.dm"
+#include "code\datums\state_machine\state.dm"
+#include "code\datums\state_machine\transition.dm"
#include "code\datums\tips\tips.dm"
#include "code\datums\trading\_trading_defines.dm"
#include "code\datums\trading\ai.dm"
@@ -1052,6 +1057,7 @@
#include "code\game\objects\items\tajara.dm"
#include "code\game\objects\items\toys.dm"
#include "code\game\objects\items\trash.dm"
+#include "code\game\objects\items\umbrella.dm"
#include "code\game\objects\items\vaurca.dm"
#include "code\game\objects\items\vitals_monitor.dm"
#include "code\game\objects\items\devices\aicard.dm"
@@ -1419,6 +1425,9 @@
#include "code\game\verbs\who.dm"
#include "code\js\byjax.dm"
#include "code\js\menus.dm"
+#include "code\modules\abstract\_abstract.dm"
+#include "code\modules\abstract\abstract_exterior_marker.dm"
+#include "code\modules\abstract\abstract_weather_marker.dm"
#include "code\modules\acting\acting_items.dm"
#include "code\modules\admin\admin.dm"
#include "code\modules\admin\admin_attack_log.dm"
@@ -3558,6 +3567,15 @@
#include "code\modules\ventcrawl\ventcrawl.dm"
#include "code\modules\ventcrawl\ventcrawl_atmospherics.dm"
#include "code\modules\ventcrawl\ventcrawl_verb.dm"
+#include "code\modules\weather\_weather.dm"
+#include "code\modules\weather\weather_debug.dm"
+#include "code\modules\weather\weather_effects.dm"
+#include "code\modules\weather\weather_fsm.dm"
+#include "code\modules\weather\weather_fsm_state_transitions.dm"
+#include "code\modules\weather\weather_fsm_states.dm"
+#include "code\modules\weather\weather_init.dm"
+#include "code\modules\weather\weather_mob_tracking.dm"
+#include "code\modules\weather\weather_wind.dm"
#include "code\modules\web_interface\webint_procs.dm"
#include "code\modules\world_api\api_command.dm"
#include "code\modules\world_api\helpers.dm"
diff --git a/code/ZAS/Turf.dm b/code/ZAS/Turf.dm
index 72c4cff18d0..e78d0054534 100644
--- a/code/ZAS/Turf.dm
+++ b/code/ZAS/Turf.dm
@@ -6,9 +6,9 @@
/turf/simulated/proc/update_graphic(list/graphic_add = null, list/graphic_remove = null)
if(graphic_add && LAZYLEN(graphic_add))
- vis_contents += graphic_add
+ add_vis_contents(graphic_add)
if(graphic_remove && LAZYLEN(graphic_remove))
- vis_contents -= graphic_remove
+ remove_vis_contents(graphic_remove)
/turf/proc/update_air_properties()
var/block
diff --git a/code/__DEFINES/misc.dm b/code/__DEFINES/misc.dm
index 326c21193d6..be2495aac44 100644
--- a/code/__DEFINES/misc.dm
+++ b/code/__DEFINES/misc.dm
@@ -505,3 +505,18 @@ example:
#define GEAR_TWEAK_ACCESSORY_SLOT_SUIT "Suit"
/// Spawns standalone in the suit slot
#define GEAR_TWEAK_ACCESSORY_SLOT_SUIT_STANDALONE "Standalone Suit"
+
+//Turf/area values for 'this space is outside' checks
+#define OUTSIDE_AREA null
+#define OUTSIDE_NO FALSE
+#define OUTSIDE_YES TRUE
+#define OUTSIDE_UNCERTAIN null
+
+// Weather exposure values for being rained on or hailed on.
+#define WEATHER_IGNORE -1
+#define WEATHER_EXPOSED 0
+#define WEATHER_ROOFED 1
+#define WEATHER_PROTECTED 2
+
+// arbitrary low pressure bound for wind weather effects
+#define MIN_WIND_PRESSURE 10
diff --git a/code/__DEFINES/subsystem-priority.dm b/code/__DEFINES/subsystem-priority.dm
index 1e7ac02b016..5a17e0f0082 100644
--- a/code/__DEFINES/subsystem-priority.dm
+++ b/code/__DEFINES/subsystem-priority.dm
@@ -49,6 +49,7 @@
#define SS_PRIORITY_EFFECTS 20 // New-style effects manager. Timing of effects may be off if this gets too far behind.
#define SS_PRIORITY_CHEMISTRY 10 // Multi-tick chemical reactions.
#define SS_PRIORITY_SHUTTLE 10 // Shuttle movement.
+#define SS_PRIORITY_WEATHER 10 // Weather processing.
#define SS_PRIORITY_AIRFLOW 10 // Handles object movement due to ZAS airflow.
#define SS_PRIORITY_ZCOPY 10 // Z-mimic icon generation/updates.
#define SS_PRIORITY_ARRIVALS 10 // Centcomm arrivals shuttle auto-launch. Usually asleep.
diff --git a/code/__DEFINES/subsystems.dm b/code/__DEFINES/subsystems.dm
index cf517475bfa..3c8eb2ac3cb 100644
--- a/code/__DEFINES/subsystems.dm
+++ b/code/__DEFINES/subsystems.dm
@@ -237,8 +237,10 @@
#define INIT_ORDER_VOTE -4
#define INIT_ORDER_ASSETS -5
#define INIT_ORDER_ICON_UPDATE -5.5 //Yet another aurora snowflake, Icon update queue flush. Should run before overlays, probably before smoothing the icon too
+#define INIT_ORDER_VISCONTENTS -5.6 // Queued vis_contents updates.
#define INIT_ORDER_ICON_SMOOTHING -6
#define INIT_ORDER_OVERLAY -7
+#define INIT_ORDER_WEATHER -9
#define INIT_ORDER_LIGHTING -20
#define INIT_ORDER_ZCOPY -21 //Aurora snowflake, Z-mimic flush. Should run after SSoverlay & SSicon_smooth so it copies the smoothed sprites.
#define INIT_ORDER_PATH -50
diff --git a/code/_onclick/hud/radial.dm b/code/_onclick/hud/radial.dm
index a9ac5b43900..c007f121b5e 100644
--- a/code/_onclick/hud/radial.dm
+++ b/code/_onclick/hud/radial.dm
@@ -260,7 +260,7 @@ GLOBAL_LIST_EMPTY(radial_menus)
//Blank
menu_holder = image(icon = 'icons/effects/effects.dmi', loc = anchor, icon_state = "nothing", layer = HUD_LAYER)
menu_holder.appearance_flags |= KEEP_APART|RESET_ALPHA|RESET_COLOR|RESET_TRANSFORM
- menu_holder.vis_contents += elements + close_button
+ menu_holder.add_vis_contents(elements + close_button)
current_user.images += menu_holder
/datum/radial_menu/proc/hide()
diff --git a/code/_onclick/hud/screen_objects.dm b/code/_onclick/hud/screen_objects.dm
index 726cfec6a11..5c21866f2f6 100644
--- a/code/_onclick/hud/screen_objects.dm
+++ b/code/_onclick/hud/screen_objects.dm
@@ -205,7 +205,7 @@
if(hovering_choice == choice)
return
- vis_contents -= hover_overlays_cache[hovering_choice]
+ remove_vis_contents(hover_overlays_cache[hovering_choice])
hovering_choice = choice
var/obj/effect/overlay/zone_sel/overlay_object = hover_overlays_cache[choice]
@@ -213,7 +213,7 @@
overlay_object = new
overlay_object.icon_state = "[choice]"
hover_overlays_cache[choice] = overlay_object
- vis_contents += overlay_object
+ add_vis_contents(overlay_object)
/obj/effect/overlay/zone_sel
@@ -225,7 +225,7 @@
/obj/screen/zone_sel/MouseExited(location, control, params)
if(!isobserver(usr) && hovering_choice)
- vis_contents -= hover_overlays_cache[hovering_choice]
+ remove_vis_contents(hover_overlays_cache[hovering_choice])
hovering_choice = null
/obj/screen/zone_sel/proc/get_zone_at(icon_x, icon_y)
diff --git a/code/controllers/subsystems/vis_contents.dm b/code/controllers/subsystems/vis_contents.dm
new file mode 100644
index 00000000000..57787d0576d
--- /dev/null
+++ b/code/controllers/subsystems/vis_contents.dm
@@ -0,0 +1,93 @@
+SUBSYSTEM_DEF(vis_contents_update)
+ name = "Vis Contents"
+ flags = SS_BACKGROUND
+ wait = 1
+ init_order = INIT_ORDER_VISCONTENTS
+ runlevels = RUNLEVEL_LOBBY | RUNLEVELS_DEFAULT
+ var/list/queue_refs = list()
+
+/datum/controller/subsystem/vis_contents_update/stat_entry()
+ ..("Queue: [queue_refs.len]")
+
+/datum/controller/subsystem/vis_contents_update/Initialize()
+ fire(FALSE, TRUE)
+ return SS_INIT_SUCCESS
+
+/datum/controller/subsystem/vis_contents_update/StartLoadingMap()
+ can_fire = FALSE
+
+/datum/controller/subsystem/vis_contents_update/StopLoadingMap()
+ can_fire = TRUE
+
+// Largely copied from SSicon_update.
+/datum/controller/subsystem/vis_contents_update/fire(resumed = FALSE, no_mc_tick = FALSE)
+ if(!queue_refs.len)
+ can_fire = FALSE
+ return
+ var/i = 0
+ while (i < queue_refs.len)
+ i++
+ var/atom/A = queue_refs[i]
+ if(QDELETED(A))
+ continue
+ if(Master.map_loading)
+ queue_refs.Cut(1, i+1)
+ return
+ A.vis_update_queued = FALSE
+ A.update_vis_contents(force_no_queue = TRUE)
+ if (no_mc_tick)
+ CHECK_TICK
+ else if (MC_TICK_CHECK)
+ queue_refs.Cut(1, i+1)
+ return
+ queue_refs.Cut()
+
+/atom
+ var/vis_update_queued = FALSE
+
+/atom/proc/queue_vis_contents_update()
+ if(vis_update_queued)
+ return
+ vis_update_queued = TRUE
+ SSvis_contents_update.queue_refs.Add(src)
+ SSvis_contents_update.can_fire = TRUE
+
+// Horrible colon syntax below is because vis_contents
+// exists in /atom.vars, but will not compile. No idea why.
+/atom/proc/add_vis_contents(adding)
+ src:vis_contents |= adding
+
+/atom/proc/remove_vis_contents(removing)
+ src:vis_contents -= removing
+
+/atom/proc/clear_vis_contents()
+ src:vis_contents = null
+
+/atom/proc/set_vis_contents(list/adding)
+ src:vis_contents = adding
+
+/atom/proc/get_vis_contents_to_add()
+ return
+
+/atom/proc/update_vis_contents(force_no_queue = FALSE)
+ if(!force_no_queue && (!SSvis_contents_update.initialized || TICK_CHECK))
+ queue_vis_contents_update()
+ return
+ vis_update_queued = FALSE
+ var/new_vis_contents = get_vis_contents_to_add()
+ if(length(new_vis_contents))
+ set_vis_contents(new_vis_contents)
+ else if(length(src:vis_contents))
+ clear_vis_contents()
+
+/image/proc/add_vis_contents(adding)
+ vis_contents |= adding
+
+/image/proc/remove_vis_contents(removing)
+ vis_contents -= removing
+
+/image/proc/clear_vis_contents()
+ vis_contents.Cut()
+
+/image/proc/set_vis_contents(list/adding)
+ vis_contents = adding
diff --git a/code/controllers/subsystems/weather.dm b/code/controllers/subsystems/weather.dm
new file mode 100644
index 00000000000..4196f51905c
--- /dev/null
+++ b/code/controllers/subsystems/weather.dm
@@ -0,0 +1,63 @@
+SUBSYSTEM_DEF(weather)
+ name = "Weather"
+ wait = 15 SECONDS
+ init_order = INIT_ORDER_WEATHER
+ priority = SS_PRIORITY_WEATHER
+ flags = SS_BACKGROUND
+
+ var/list/weather_systems = list()
+ var/list/weather_by_z = list()
+ var/list/processing_systems
+
+/datum/controller/subsystem/weather/stat_entry()
+ ..("all systems: [length(weather_systems)], processing systems: [length(processing_systems)]")
+
+/datum/controller/subsystem/weather/Initialize(start_timeofday)
+ . = ..()
+ for(var/obj/abstract/weather_system/weather as anything in weather_systems)
+ weather.init_weather()
+ return SS_INIT_SUCCESS
+
+/datum/controller/subsystem/weather/fire(resumed)
+
+ if(!resumed)
+ processing_systems = weather_systems.Copy()
+
+ var/obj/abstract/weather_system/weather
+ while(processing_systems.len)
+ weather = processing_systems[processing_systems.len]
+ processing_systems.len--
+ weather.tick()
+ if(MC_TICK_CHECK)
+ return
+
+///Sets a weather state to use for a given z level/z level stack.
+/datum/controller/subsystem/weather/proc/setup_weather_system(var/topmost_level, var/singleton/state/weather/initial_state)
+ //First check and clear any existing weather system on the level
+ var/obj/abstract/weather_system/WS = weather_by_z["[topmost_level]"]
+ if(WS)
+ unregister_weather_system(WS)
+ qdel(WS)
+ //Create the new weather system and let it register itself
+ new /obj/abstract/weather_system(locate(1, 1, text2num(topmost_level)), topmost_level, initial_state)
+
+///Registers a given weather system obj for getting updates by SSweather.
+/datum/controller/subsystem/weather/proc/register_weather_system(var/obj/abstract/weather_system/WS)
+ if(weather_by_z["[WS.z]"])
+ CRASH("Trying to register another weather system on the same z-level([WS.z]) as an existing one!")
+ weather_systems |= WS
+
+ //Mark all affected z-levels
+ var/list/affected = GetConnectedZlevels(WS.z)
+ for(var/Z in affected)
+ if(weather_by_z["[Z]"])
+ CRASH("Trying to register another weather system on the same z-level([Z]) as an existing one!")
+ weather_by_z["[Z]"] = WS
+
+///Remove a weather systeam from the processing lists.
+/datum/controller/subsystem/weather/proc/unregister_weather_system(var/obj/abstract/weather_system/WS)
+ //Clear any and all references to our weather object
+ for(var/Z = 1 to length(weather_by_z))
+ if(weather_by_z["[Z]"] == WS)
+ weather_by_z["[Z]"] = null
+ weather_systems -= WS
diff --git a/code/datums/datum.dm b/code/datums/datum.dm
index 7d422e90ce8..26512578846 100644
--- a/code/datums/datum.dm
+++ b/code/datums/datum.dm
@@ -34,6 +34,9 @@
/// A weak reference to another datum
var/datum/weakref/weak_reference
+ /// Used to avoid unnecessary refstring creation in Destroy().
+ var/tmp/has_state_machine = FALSE
+
#ifdef REFERENCE_TRACKING
/// When was this datum last touched by a reftracker?
/// If this value doesn't match with the start of the search
diff --git a/code/datums/repositories/sound_channels.dm b/code/datums/repositories/sound_channels.dm
index 9a11339f9ad..4529bb0d9d9 100644
--- a/code/datums/repositories/sound_channels.dm
+++ b/code/datums/repositories/sound_channels.dm
@@ -12,9 +12,12 @@ var/repository/sound_channels/sound_channels = new()
var/list/keys_by_channel // So we know who to blame if we run out
var/channel_ceiling = 1024 // Initial value is the current BYOND maximum number of channels
+ var/weather_channel
+
/repository/sound_channels/New()
..()
available_channels = new()
+ weather_channel = RequestChannel("WEATHER")
/repository/sound_channels/proc/RequestChannel(key)
. = RequestChannels(key, 1)
diff --git a/code/datums/state_machine.dm b/code/datums/state_machine.dm
new file mode 100644
index 00000000000..85bc10eb0b5
--- /dev/null
+++ b/code/datums/state_machine.dm
@@ -0,0 +1,104 @@
+// List and procs for caching state machine instances.
+var/global/list/state_machines = list()
+
+/proc/get_state_machine(var/datum/holder, var/base_type)
+ if(istype(holder) && base_type && holder.has_state_machine)
+ var/list/machines = global.state_machines["\ref[holder]"]
+ return islist(machines) && machines[base_type]
+
+/proc/add_state_machine(var/datum/holder, var/base_type, var/fsm_type)
+ if(istype(holder) && base_type)
+ var/holder_ref = "\ref[holder]"
+ var/list/machines = global.state_machines[holder_ref]
+ if(!islist(machines))
+ machines = list()
+ global.state_machines[holder_ref] = machines
+ if(!machines[base_type])
+ if(!fsm_type)
+ fsm_type = base_type
+ var/datum/state_machine/machine = new fsm_type(holder)
+ machines[base_type] = machine
+ holder.has_state_machine = TRUE
+ return machine
+
+/proc/remove_state_machine(var/datum/holder, var/base_type)
+ if(istype(holder) && base_type && holder.has_state_machine)
+ var/holder_ref = "\ref[holder]"
+ var/list/machines = global.state_machines[holder_ref]
+ if(length(machines))
+ machines -= base_type
+ if(!length(machines))
+ global.state_machines -= holder_ref
+ holder.has_state_machine = FALSE
+ return TRUE
+ return FALSE
+
+// This contains the current state of the FSM and should be held by whatever the FSM is controlling.
+// Unlike the individual states and their transitions, the state machine objects are not singletons, and hence aren't `/decl`s.
+/datum/state_machine
+ var/datum/weakref/holder_ref
+ var/base_type = /datum/state_machine
+ var/expected_type = /datum
+ var/singleton/state/current_state = null // Acts both as a ref to the current state and holds which state it will default to on init.
+
+/datum/state_machine/New(var/datum/_holder)
+ ..()
+ if(!istype(_holder))
+ stack_trace("Non-datum holder supplied to [type] New().")
+ else
+ holder_ref = WEAKREF(_holder)
+ set_state(current_state)
+
+/datum/state_machine/Destroy()
+ current_state = null
+ return ..()
+
+// Resets back to our initial state.
+/datum/state_machine/proc/reset()
+ var/datum/holder_instance = get_holder()
+ if(istype(current_state))
+ current_state.exited_state(holder_instance)
+ current_state = initial(current_state)
+ if(ispath(current_state, /singleton/state))
+ current_state = GET_SINGLETON(current_state)
+ current_state.entered_state(holder_instance)
+ else
+ current_state = null
+ return current_state
+
+// Retrieve and validate our holder instance from the cached weakref.
+/datum/state_machine/proc/get_holder()
+ var/datum/holder = holder_ref?.resolve()
+ if(istype(holder) && !QDELETED(holder))
+ return holder
+
+// Makes the FSM enter a new state, if it can, based on it's current state, that state's transitions, and the holder's status.
+// Call it in the holder's `process()`, or whenever you need to.
+/datum/state_machine/proc/evaluate()
+ var/datum/holder_instance = get_holder()
+ var/list/options = current_state.get_open_transitions(holder_instance)
+ if(LAZYLEN(options))
+ var/singleton/state_transition/choice = choose_transition(options)
+ current_state.exited_state(holder_instance)
+ current_state = choice.target
+ current_state.entered_state(holder_instance)
+ return current_state
+
+// Decides which transition to walk into, to the next state.
+// By default it chooses the first one on the list.
+/datum/state_machine/proc/choose_transition(list/valid_transitions)
+ return valid_transitions[1]
+
+// Forces the FSM to switch to a specific state, no matter what.
+// Use responsibly.
+/datum/state_machine/proc/set_state(new_state_type)
+ var/datum/holder_instance = get_holder()
+ if(istype(current_state))
+ current_state.exited_state(holder_instance)
+ if(ispath(new_state_type))
+ current_state = GET_SINGLETON(new_state_type)
+ else // need to include null here, so we can't do an istype
+ current_state = new_state_type
+ if(istype(current_state))
+ current_state.entered_state(holder_instance)
+ return current_state
diff --git a/code/datums/state_machine/state.dm b/code/datums/state_machine/state.dm
new file mode 100644
index 00000000000..8c926b44b75
--- /dev/null
+++ b/code/datums/state_machine/state.dm
@@ -0,0 +1,30 @@
+// An individual state, defined as a `/singleton` to save memory.
+// On a directed graph, these would be the nodes themselves, connected to each other by unidirectional arrows.
+/singleton/state
+ // Transition decl types, which get turned into refs to those types.
+ // Note that the order DOES matter, as decls earlier in the list have higher priority
+ // if more than one becomes 'open'.
+ var/list/transitions = null
+
+/singleton/state/Initialize()
+ . = ..()
+ for(var/i in 1 to LAZYLEN(transitions))
+ var/singleton/state_transition/T = GET_SINGLETON(transitions[i])
+ T.from += src
+ transitions[i] = T
+
+// Returns a list of transitions that a FSM could switch to.
+// Note that `holder` is NOT the FSM, but instead the thing the FSM is attached to.
+/singleton/state/proc/get_open_transitions(datum/holder)
+ for(var/singleton/state_transition/T as anything in transitions)
+ if(T.is_open(holder))
+ LAZYADD(., T)
+
+// Stub for child states to modify the holder when switched to.
+// Again, `holder` is not the FSM.
+/singleton/state/proc/entered_state(datum/holder)
+ return
+
+// Another stub for when leaving a state.
+/singleton/state/proc/exited_state(datum/holder)
+ return
diff --git a/code/datums/state_machine/transition.dm b/code/datums/state_machine/transition.dm
new file mode 100644
index 00000000000..1467ad99352
--- /dev/null
+++ b/code/datums/state_machine/transition.dm
@@ -0,0 +1,16 @@
+// Used to connect `/singleton/state`s together so the FSM knows what state to switch to, and on what conditions.
+// On a directed graph, these would be the arrows connecting the nodes representing states.
+/singleton/state_transition
+ var/list/from = null
+ var/singleton/state/target = null
+
+// Called by one or more state decls acting as nodes in a directed graph.
+/singleton/state_transition/Initialize()
+ . = ..()
+ LAZYINITLIST(from)
+ if(ispath(target))
+ target = GET_SINGLETON(target)
+
+// Tells the FSM if it should or should not be allowed to transfer to the target state.
+/singleton/state_transition/proc/is_open(datum/holder)
+ return FALSE
diff --git a/code/game/area/areas.dm b/code/game/area/areas.dm
index 230633386fd..56ee7def3b2 100644
--- a/code/game/area/areas.dm
+++ b/code/game/area/areas.dm
@@ -72,6 +72,8 @@ var/global/list/area_blurb_stated_to = list()
/// Used to filter description showing across subareas.
var/area_blurb_category
+ var/tmp/is_outside = OUTSIDE_NO
+
// Don't move this to Initialize(). Things in here need to run before SSatoms does.
/area/New()
// DMMS hook - Required for areas to work properly.
@@ -462,6 +464,7 @@ var/list/mob/living/forced_ambiance_list = new
/proc/ChangeArea(var/turf/T, var/area/A)
if(!istype(A))
CRASH("Area change attempt failed: invalid area supplied.")
+ var/old_outside = T.is_outside()
var/area/old_area = get_area(T)
if(old_area == A)
return
@@ -477,6 +480,11 @@ var/list/mob/living/forced_ambiance_list = new
for(var/obj/machinery/M in T)
M.shuttle_move(T)
+ T.last_outside_check = OUTSIDE_UNCERTAIN
+ var/outside_changed = T.is_outside() != old_outside
+ if(T.is_outside == OUTSIDE_AREA && outside_changed)
+ T.update_weather()
+
/**
* Displays an area blurb on a mob's screen.
*
diff --git a/code/game/jobs/faction/faction.dm b/code/game/jobs/faction/faction.dm
index 6b0b98a41fa..70eaf663f50 100644
--- a/code/game/jobs/faction/faction.dm
+++ b/code/game/jobs/faction/faction.dm
@@ -40,6 +40,8 @@
. += role
else
var/datum/job/role = SSjobs.type_occupations[path]
+ if(!role)
+ continue
if(LAZYACCESS(job_species_blacklist, role.title))
role.blacklisted_species = job_species_blacklist[role.title]
else
diff --git a/code/game/objects/items.dm b/code/game/objects/items.dm
index e056e77c48c..fa0afea637c 100644
--- a/code/game/objects/items.dm
+++ b/code/game/objects/items.dm
@@ -1227,3 +1227,6 @@ modules/mob/living/carbon/human/life.dm if you die, you will be zoomed out.
/obj/item/proc/is_shovel()
return FALSE
+
+/obj/item/proc/gives_weather_protection()
+ return FALSE
diff --git a/code/game/objects/items/sticker.dm b/code/game/objects/items/sticker.dm
index 650e81ae978..bb2d48f76e8 100644
--- a/code/game/objects/items/sticker.dm
+++ b/code/game/objects/items/sticker.dm
@@ -62,14 +62,14 @@
to_chat(user, SPAN_NOTICE("You attach \the [src] to \the [A]."))
user.drop_from_inventory(src, A)
attached = WEAKREF(A)
- A.vis_contents += src
+ A.add_vis_contents(src)
/obj/item/sticker/proc/remove_sticker(var/mob/user)
user.put_in_hands(src)
var/atom/movable/attached_atom = attached.resolve()
if(attached_atom)
to_chat(user, SPAN_NOTICE("You remove \the [src] from \the [attached_atom]."))
- attached_atom.vis_contents -= src
+ attached_atom.remove_vis_contents(src)
attached = null
/obj/item/sticker/googly_eye
diff --git a/code/game/objects/items/umbrella.dm b/code/game/objects/items/umbrella.dm
new file mode 100644
index 00000000000..72d36386daa
--- /dev/null
+++ b/code/game/objects/items/umbrella.dm
@@ -0,0 +1,65 @@
+/obj/item/umbrella
+ name = "umbrella"
+ desc = "A perfect tool to protect you from the elements."
+ icon = 'icons/obj/item/umbrellas.dmi'
+ icon_state = "umbrella_yellow_closed"
+ contained_sprite = TRUE
+ w_class = ITEMSIZE_SMALL
+ matter = list(MATERIAL_PLASTIC = 1000)
+ force = 5
+ sharp = TRUE
+ hitsound = 'sound/weapons/bladeslice.ogg'
+ attack_verb = list("pokes", "stabs")
+ /// If the umbrella is open or not.
+ var/is_open = FALSE
+ /// Colour of the umbrella. Must be one present in the dmi.
+ var/umbrella_color
+
+/obj/item/umbrella/Initialize(mapload, ...)
+ . = ..()
+ if(!umbrella_color)
+ umbrella_color = pick("black", "red", "yellow", "green")
+ icon_state = "umbrella_[umbrella_color]"
+ item_state = icon_state + "_closed"
+ update_icon()
+
+/obj/item/umbrella/attack_self(mob/user)
+ . = ..()
+ if(!user.incapacitated())
+ if(!is_open)
+ to_chat(user, SPAN_NOTICE("You unfurl \the [src]."))
+ item_state = "umbrella_[umbrella_color]_open"
+ w_class = ITEMSIZE_LARGE
+ is_open = TRUE
+ force = 1
+ sharp = FALSE
+ attack_verb = list("taps")
+ hitsound = /singleton/sound_category/punchmiss_sound
+ else
+ to_chat(user, SPAN_NOTICE("You close up \the [src]."))
+ item_state = "umbrella_[umbrella_color]_closed"
+ w_class = initial(w_class)
+ is_open = FALSE
+ force = initial(force)
+ sharp = initial(sharp)
+ attack_verb = initial(attack_verb)
+ hitsound = initial(hitsound)
+
+/obj/item/umbrella/gives_weather_protection()
+ return is_open ? TRUE : FALSE
+
+/obj/item/umbrella/red
+ umbrella_color = "red"
+ icon_state = "umbrella_red_closed"
+
+/obj/item/umbrella/black
+ umbrella_color = "black"
+ icon_state = "umbrella_black_closed"
+
+/obj/item/umbrella/yellow
+ umbrella_color = "yellow"
+ icon_state = "umbrella_yellow_closed"
+
+/obj/item/umbrella/green
+ umbrella_color = "green"
+ icon_state = "umbrella_green_closed"
diff --git a/code/game/objects/items/weapons/storage/storage.dm b/code/game/objects/items/weapons/storage/storage.dm
index b4e78be2b68..4102526d646 100644
--- a/code/game/objects/items/weapons/storage/storage.dm
+++ b/code/game/objects/items/weapons/storage/storage.dm
@@ -432,7 +432,7 @@
stored_end.transform = M_end
storage_screens += list(stored_start, stored_continue, stored_end)
- storage_start.vis_contents += list(stored_start, stored_continue, stored_end)
+ storage_start.add_vis_contents(list(stored_start, stored_continue, stored_end))
O.screen_loc = "4:[round((startpoint+endpoint)/2)+2],2:16"
O.maptext = ""
diff --git a/code/game/objects/structures/crates_lockers/closets.dm b/code/game/objects/structures/crates_lockers/closets.dm
index df20f2f7f5a..dc5e738fd55 100644
--- a/code/game/objects/structures/crates_lockers/closets.dm
+++ b/code/game/objects/structures/crates_lockers/closets.dm
@@ -608,7 +608,7 @@
/obj/structure/closet/proc/end_door_animation()
is_animating_door = FALSE // comment this out and the line below to manually tweak the animation end state by fiddling with the door_anim vars to match the open door icon
- vis_contents -= door_obj
+ remove_vis_contents(door_obj)
update_icon()
compile_overlays(src)
@@ -639,7 +639,7 @@
/obj/structure/closet/proc/end_door_animation_alt()
is_animating_door = FALSE // comment this out and the line below to manually tweak the animation end state by fiddling with the door_anim vars to match the open door icon
- vis_contents -= door_obj_alt
+ remove_vis_contents(door_obj_alt)
update_icon()
compile_overlays(src)
diff --git a/code/game/objects/structures/mirror.dm b/code/game/objects/structures/mirror.dm
index 13d3ed83bf9..a395478c465 100644
--- a/code/game/objects/structures/mirror.dm
+++ b/code/game/objects/structures/mirror.dm
@@ -148,7 +148,7 @@
for(var/mob/living/carbon/human/H in loc)
check_vampire_enter(H.loc, H)
- vis_contents += get_turf(mirror)
+ add_vis_contents(get_turf(mirror))
/obj/effect/reflection/proc/check_vampire_enter(var/turf/T, var/mob/living/carbon/human/H)
if(!istype(H))
diff --git a/code/game/objects/structures/stool_bed_chair_nest/bed.dm b/code/game/objects/structures/stool_bed_chair_nest/bed.dm
index 954c5369af5..f5f2bf084f7 100644
--- a/code/game/objects/structures/stool_bed_chair_nest/bed.dm
+++ b/code/game/objects/structures/stool_bed_chair_nest/bed.dm
@@ -415,7 +415,7 @@
add_overlay(light)
if(vitals)
vitals.update_monitor()
- vis_contents += vitals
+ add_vis_contents(vitals)
/obj/structure/bed/roller/attackby(obj/item/attacking_item, mob/user)
if(iswrench(attacking_item) || istype(attacking_item, /obj/item/stack) || iswirecutter(attacking_item))
diff --git a/code/game/turfs/simulated/floor.dm b/code/game/turfs/simulated/floor.dm
index 0de988c20c5..30612dc6b38 100644
--- a/code/game/turfs/simulated/floor.dm
+++ b/code/game/turfs/simulated/floor.dm
@@ -3,6 +3,7 @@
desc = "The naked hull."
icon = 'icons/turf/flooring/plating.dmi'
icon_state = "plating"
+ is_outside = OUTSIDE_AREA
// Damage to flooring.
var/broken
diff --git a/code/game/turfs/turf.dm b/code/game/turfs/turf.dm
index 87f1da81840..7737047c69f 100644
--- a/code/game/turfs/turf.dm
+++ b/code/game/turfs/turf.dm
@@ -62,6 +62,23 @@
/// WARNING: Currently to use a density shortcircuiting this does not support dense turfs with special allow through function
var/pathing_pass_method = TURF_PATHING_PASS_DENSITY
+ // Some quick notes on the vars below: is_outside should be left set to OUTSIDE_AREA unless you
+ // EXPLICITLY NEED a turf to have a different outside state to its area (ie. you have used a
+ // roofing tile). By default, it will ask the area for the state to use, and will update on
+ // area change. When dealing with weather, it will check the entire z-column for interruptions
+ // that will prevent it from using its own state, so a floor above a level will generally
+ // override both area is_outside, and turf is_outside. The only time the base value will be used
+ // by itself is if you are dealing with a non-multiz level, or the top level of a multiz chunk.
+
+ // Weather relies on is_outside to determine if it should apply to a turf or not and will be
+ // automatically updated on ChangeTurf set_outside etc. Don't bother setting it manually, it will
+ // get overridden almost immediately.
+
+ // TL;DR: just leave these vars alone.
+ var/tmp/obj/abstract/weather_system/weather
+ var/tmp/is_outside = OUTSIDE_AREA
+ var/tmp/last_outside_check = OUTSIDE_UNCERTAIN
+
// Parent code is duplicated in here instead of ..() for performance reasons.
// There's ALSO a copy of this in mine_turfs.dm!
/turf/Initialize(mapload, ...)
@@ -516,6 +533,89 @@ var/const/enterloopsanity = 100
/turf/proc/is_floor()
return FALSE
+/turf/proc/is_outside()
+
+ // Can't rain inside or through solid walls.
+ // TODO: dense structures like full windows should probably also block weather.
+ if(density)
+ return OUTSIDE_NO
+
+ if(last_outside_check != OUTSIDE_UNCERTAIN)
+ return last_outside_check
+
+ // What is our local outside value?
+ // Some turfs can be roofed irrespective of the turf above them in multiz.
+ // I have the feeling this is redundat as a roofed turf below max z will
+ // have a floor above it, but ah well.
+ . = is_outside
+ if(. == OUTSIDE_AREA)
+ var/area/A = get_area(src)
+ . = A ? A.is_outside : OUTSIDE_NO
+
+ // If we are in a multiz volume and not already inside, we return
+ // the outside value of the highest unenclosed turf in the stack.
+ if(HasAbove(z))
+ . = OUTSIDE_YES // assume for the moment we're unroofed until we learn otherwise.
+ var/turf/top_of_stack = src
+ while(HasAbove(top_of_stack.z))
+ var/turf/next_turf = GetAbove(top_of_stack)
+ if(!next_turf.is_open())
+ return OUTSIDE_NO
+ top_of_stack = next_turf
+ // If we hit the top of the stack without finding a roof, we ask the upmost turf if we're outside.
+ . = top_of_stack.is_outside()
+ last_outside_check = . // Cache this for later calls.
+
+/turf/proc/set_outside(var/new_outside, var/skip_weather_update = FALSE)
+ if(is_outside == new_outside)
+ return FALSE
+
+ is_outside = new_outside
+ if(!skip_weather_update)
+ update_weather()
+
+ last_outside_check = OUTSIDE_UNCERTAIN
+
+ if(!HasBelow(z))
+ return TRUE
+
+ // Invalidate the outside check cache for turfs below us.
+ var/turf/checking = src
+ while(HasBelow(checking.z))
+ checking = GetBelow(checking)
+ if(!isturf(checking))
+ break
+ checking.last_outside_check = OUTSIDE_UNCERTAIN
+ if(!checking.is_open())
+ break
+ return TRUE
+
+/turf/proc/update_weather(var/obj/abstract/weather_system/new_weather, var/force_update_below = FALSE)
+
+ if(isnull(new_weather))
+ new_weather = SSweather.weather_by_z["[z]"]
+
+ // We have a weather system and we are exposed to it; update our vis contents.
+ if(istype(new_weather) && is_outside())
+ if(weather != new_weather)
+ if(weather)
+ remove_vis_contents(weather.vis_contents_additions)
+ weather = new_weather
+ add_vis_contents(weather.vis_contents_additions)
+ . = TRUE
+
+ // We are indoors or there is no local weather system, clear our vis contents.
+ else if(weather)
+ remove_vis_contents(weather.vis_contents_additions)
+ weather = null
+ . = TRUE
+
+ // Propagate our weather downwards if we permit it.
+ if(force_update_below || (is_open() && .))
+ var/turf/below = GetBelow(src)
+ if(below)
+ below.update_weather(new_weather)
+
/turf/proc/remove_cleanables()
for(var/obj/effect/O in src)
if(istype(O,/obj/effect/rune) || istype(O,/obj/effect/decal/cleanable))
diff --git a/code/game/turfs/turf_changing.dm b/code/game/turfs/turf_changing.dm
index 1361ef09022..7cdd4ca0650 100644
--- a/code/game/turfs/turf_changing.dm
+++ b/code/game/turfs/turf_changing.dm
@@ -52,6 +52,8 @@
var/list/old_corners = corners
var/list/old_blueprints = blueprints
var/list/old_decals = decals
+ var/old_outside = is_outside
+ var/old_is_open = is_open()
changing_turf = TRUE
@@ -105,6 +107,11 @@
if(tell_universe)
GLOB.universe.OnTurfChange(W)
+ // we check the var rather than the proc, because area outside values usually shouldn't be set on turfs
+ W.last_outside_check = OUTSIDE_UNCERTAIN
+ if(W.is_outside != old_outside)
+ W.set_outside(old_outside, skip_weather_update = TRUE)
+
SSair.mark_for_update(src) //handle the addition of the new turf.
if(!W.baseturf)
@@ -119,6 +126,8 @@
W.post_change(!mapload)
+ W.update_weather(force_update_below = W.is_open() != old_is_open)
+
. = W
for(var/turf/T in RANGE_TURFS(1, src))
diff --git a/code/modules/abstract/_abstract.dm b/code/modules/abstract/_abstract.dm
new file mode 100644
index 00000000000..8c9bf080c84
--- /dev/null
+++ b/code/modules/abstract/_abstract.dm
@@ -0,0 +1,21 @@
+/obj/abstract
+ name = ""
+ icon = 'icons/effects/landmarks.dmi'
+ icon_state = "x2"
+ simulated = FALSE
+ density = FALSE
+ anchored = TRUE
+ abstract_type = /obj/abstract
+ invisibility = INVISIBILITY_ABSTRACT
+
+/obj/abstract/Initialize()
+ . = ..()
+ verbs.Cut()
+ //Let mappers see the damn thing by just making them invisible here
+ opacity = FALSE
+ alpha = 0
+ mouse_opacity = MOUSE_OPACITY_TRANSPARENT
+
+/obj/abstract/ex_act()
+ SHOULD_CALL_PARENT(FALSE)
+ return
diff --git a/code/modules/abstract/abstract_exterior_marker.dm b/code/modules/abstract/abstract_exterior_marker.dm
new file mode 100644
index 00000000000..8e56ea87c75
--- /dev/null
+++ b/code/modules/abstract/abstract_exterior_marker.dm
@@ -0,0 +1,21 @@
+/obj/abstract/exterior_marker
+ var/set_outside
+
+/obj/abstract/exterior_marker/Initialize()
+ ..()
+ var/turf/T = loc
+ if(istype(T))
+ T.set_outside(set_outside)
+ return INITIALIZE_HINT_QDEL
+
+/obj/abstract/exterior_marker/outside
+ name = "Outside"
+ set_outside = OUTSIDE_NO
+
+/obj/abstract/exterior_marker/inside
+ name = "Inside"
+ set_outside = OUTSIDE_YES
+
+/obj/abstract/exterior_marker/use_area
+ name = "Use Area Outside"
+ set_outside = OUTSIDE_AREA
diff --git a/code/modules/abstract/abstract_weather_marker.dm b/code/modules/abstract/abstract_weather_marker.dm
new file mode 100644
index 00000000000..3117a79692a
--- /dev/null
+++ b/code/modules/abstract/abstract_weather_marker.dm
@@ -0,0 +1,16 @@
+/obj/abstract/weather_marker
+ name = "Weather Marker"
+ /// The weather type we want. This needs to be a weather singleton type, such as /singleton/state/weather/rain/storm.
+ /// These should be used for forcing weather in an away site or an event map.
+ var/weather_type
+
+/obj/abstract/weather_marker/Initialize()
+ ..()
+ return INITIALIZE_HINT_LATELOAD
+
+/obj/abstract/weather_marker/LateInitialize()
+ . = ..()
+ if(!ispath(weather_type))
+ log_debug("Invalid weather type mapped in at [x] [y] [z]!")
+ return INITIALIZE_HINT_QDEL
+ SSweather.setup_weather_system(z, weather_type)
diff --git a/code/modules/admin/admin_verbs.dm b/code/modules/admin/admin_verbs.dm
index abb7ef61209..43b2bf2b46b 100644
--- a/code/modules/admin/admin_verbs.dm
+++ b/code/modules/admin/admin_verbs.dm
@@ -232,7 +232,10 @@ var/list/admin_verbs_debug = list(
/turf/proc/view_chunk,
/turf/proc/update_chunk,
/client/proc/profiler_start,
- /client/proc/rustg_send_udp
+ /client/proc/rustg_send_udp,
+ /datum/admins/proc/force_initialize_weather,
+ /datum/admins/proc/force_weather_state,
+ /datum/admins/proc/force_kill_weather
)
var/list/admin_verbs_paranoid_debug = list(
diff --git a/code/modules/clothing/clothing.dm b/code/modules/clothing/clothing.dm
index c9f9bdfb051..d79d40ba219 100644
--- a/code/modules/clothing/clothing.dm
+++ b/code/modules/clothing/clothing.dm
@@ -550,6 +550,7 @@
var/light_applied
var/brightness_on
var/on = 0
+ var/protects_against_weather = FALSE
/obj/item/clothing/head/Initialize(mapload, material_key)
. = ..()
@@ -986,18 +987,19 @@
)
name = "suit"
species_sprite_adaption_type = WORN_SUIT
- var/fire_resist = T0C+100
body_parts_covered = UPPER_TORSO|LOWER_TORSO|ARMS|LEGS
allowed = list(/obj/item/tank/emergency_oxygen)
armor = null
slot_flags = SLOT_OCLOTHING
- var/blood_overlay_type = "suit"
siemens_coefficient = 0.9
w_class = ITEMSIZE_NORMAL
species_restricted = list("exclude",BODYTYPE_VAURCA_BREEDER,BODYTYPE_VAURCA_WARFORM,BODYTYPE_TESLA_BODY)
-
valid_accessory_slots = list(ACCESSORY_SLOT_ARMBAND, ACCESSORY_SLOT_GENERIC, ACCESSORY_SLOT_CAPE, ACCESSORY_SLOT_UTILITY_MINOR)
+ var/fire_resist = T0C+100
+ var/blood_overlay_type = "suit"
+ var/protects_against_weather = FALSE
+
/obj/item/clothing/suit/return_own_image()
var/image/our_image
if(contained_sprite)
diff --git a/code/modules/clothing/head/helmet.dm b/code/modules/clothing/head/helmet.dm
index 023d259b474..65725f26b76 100644
--- a/code/modules/clothing/head/helmet.dm
+++ b/code/modules/clothing/head/helmet.dm
@@ -24,6 +24,7 @@
var/obj/machinery/camera/camera
drop_sound = 'sound/items/drop/helm.ogg'
pickup_sound = 'sound/items/pickup/helm.ogg'
+ protects_against_weather = TRUE
var/has_storage = TRUE
var/obj/item/storage/internal/helmet/hold
diff --git a/code/modules/clothing/head/wide_hats.dm b/code/modules/clothing/head/wide_hats.dm
index 945399ad96b..be4a5c66c85 100644
--- a/code/modules/clothing/head/wide_hats.dm
+++ b/code/modules/clothing/head/wide_hats.dm
@@ -26,6 +26,7 @@
contained_sprite = TRUE
icon_state = "cowboyhat"
item_state = "cowboyhat"
+ protects_against_weather = TRUE
/obj/item/clothing/head/cowboy/wide
name = "wide-brimmed cowboy hat"
diff --git a/code/modules/clothing/spacesuits/spacesuits.dm b/code/modules/clothing/spacesuits/spacesuits.dm
index bc2817e0bc0..d53be02382e 100644
--- a/code/modules/clothing/spacesuits/spacesuits.dm
+++ b/code/modules/clothing/spacesuits/spacesuits.dm
@@ -64,6 +64,7 @@
max_pressure_protection = SPACE_SUIT_MAX_PRESSURE
siemens_coefficient = 0.5
species_restricted = list("exclude",BODYTYPE_DIONA,BODYTYPE_GOLEM,BODYTYPE_VAURCA_BULWARK)
+ protects_against_weather = TRUE
var/list/supporting_limbs //If not-null, automatically splints breaks. Checked when removing the suit.
diff --git a/code/modules/clothing/suits/hoodies.dm b/code/modules/clothing/suits/hoodies.dm
index 16a08228f95..01c6cb1dfd7 100644
--- a/code/modules/clothing/suits/hoodies.dm
+++ b/code/modules/clothing/suits/hoodies.dm
@@ -46,6 +46,7 @@
bio = ARMOR_BIO_MINOR
)
siemens_coefficient = 0.75
+ protects_against_weather = TRUE
/obj/item/clothing/head/winterhood
name = "winter hood"
@@ -59,6 +60,7 @@
flags_inv = HIDEEARS | BLOCKHAIR | HIDEEARS
min_cold_protection_temperature = SPACE_SUIT_MIN_COLD_PROTECTION_TEMPERATURE
canremove = 0
+ protects_against_weather = TRUE
var/hooded = FALSE
/obj/item/clothing/head/winterhood/Initialize(mapload, material_key)
diff --git a/code/modules/clothing/suits/miscellaneous.dm b/code/modules/clothing/suits/miscellaneous.dm
index effd737aaa8..c55c7fa0d59 100644
--- a/code/modules/clothing/suits/miscellaneous.dm
+++ b/code/modules/clothing/suits/miscellaneous.dm
@@ -354,6 +354,7 @@
contained_sprite = TRUE
blood_overlay_type = "coat"
body_parts_covered = UPPER_TORSO|ARMS
+ protects_against_weather = TRUE
/obj/item/clothing/suit/storage/toggle/trench/grey
name = "grey trenchcoat"
diff --git a/code/modules/cooking/trays.dm b/code/modules/cooking/trays.dm
index c6c325cd797..1fe3b9973d3 100644
--- a/code/modules/cooking/trays.dm
+++ b/code/modules/cooking/trays.dm
@@ -104,7 +104,7 @@
I.forceMove(src)
auto_align(I, click_params)
current_weight += I.w_class
- vis_contents += I
+ add_vis_contents(I)
I.vis_flags |= VIS_INHERIT_LAYER | VIS_INHERIT_PLANE
item_equipped_event.register(I, src, PROC_REF(pick_up))
GLOB.destroyed_event.register(I, src, PROC_REF(unload_item))
diff --git a/code/modules/effects/map_effects/portal.dm b/code/modules/effects/map_effects/portal.dm
index 516f5d8386a..9f92fdbd914 100644
--- a/code/modules/effects/map_effects/portal.dm
+++ b/code/modules/effects/map_effects/portal.dm
@@ -202,7 +202,7 @@ when portals are shortly lived, or when portals are made to be obvious with spec
return
var/turf/T = P.counterpart.get_focused_turf()
- P.vis_contents += T
+ P.add_vis_contents(T)
var/list/things = list()
DVIEW(things, world.view, T, INVISIBILITY_LIGHTING)
@@ -211,7 +211,7 @@ when portals are shortly lived, or when portals are made to be obvious with spec
if(turf in observed_turfs) // Avoid showing the same turf twice or more for improved performance.
continue
- P.vis_contents += turf
+ P.add_vis_contents(turf)
observed_turfs += turf
P.calculate_dimensions()
diff --git a/code/modules/heavy_vehicle/equipment/combat.dm b/code/modules/heavy_vehicle/equipment/combat.dm
index 5bf8fc77e4f..21dc5ccd3f9 100644
--- a/code/modules/heavy_vehicle/equipment/combat.dm
+++ b/code/modules/heavy_vehicle/equipment/combat.dm
@@ -379,7 +379,7 @@
/obj/aura/mechshield/added_to(mob/living/target)
..()
- target.vis_contents += src
+ target.add_vis_contents(src)
dir = target.dir
/obj/aura/mechshield/proc/set_holder(var/obj/item/mecha_equipment/shield/holder)
@@ -387,7 +387,7 @@
/obj/aura/mechshield/Destroy()
if(user)
- user.vis_contents -= src
+ user.remove_vis_contents(src)
shields = null
. = ..()
diff --git a/code/modules/heavy_vehicle/mech_life.dm b/code/modules/heavy_vehicle/mech_life.dm
index 8bc416d7cd2..5bd0df9264c 100644
--- a/code/modules/heavy_vehicle/mech_life.dm
+++ b/code/modules/heavy_vehicle/mech_life.dm
@@ -96,6 +96,7 @@
return total_draw
/mob/living/heavy_vehicle/handle_environment(var/datum/gas_mixture/environment)
+ ..()
if(!environment) return
//Mechs and vehicles in general can be assumed to just tend to whatever ambient temperature
if(abs(environment.temperature - bodytemperature) > 10 )
diff --git a/code/modules/mob/living/carbon/alien/diona/life.dm b/code/modules/mob/living/carbon/alien/diona/life.dm
index 1918d4b0881..22d280b2d8c 100644
--- a/code/modules/mob/living/carbon/alien/diona/life.dm
+++ b/code/modules/mob/living/carbon/alien/diona/life.dm
@@ -1,6 +1,7 @@
//Dionaea regenerate health and nutrition in light.
/mob/living/carbon/alien/diona/handle_environment(datum/gas_mixture/environment)
+ ..()
if(environment && consume_nutrition_from_air)
environment.remove(diona_handle_air(get_dionastats(), pressure))
diff --git a/code/modules/mob/living/carbon/alien/life.dm b/code/modules/mob/living/carbon/alien/life.dm
index ba08e1f9850..b24e70e9afb 100644
--- a/code/modules/mob/living/carbon/alien/life.dm
+++ b/code/modules/mob/living/carbon/alien/life.dm
@@ -143,6 +143,7 @@
return 1
/mob/living/carbon/alien/handle_environment(var/datum/gas_mixture/environment)
+ ..()
// Both alien subtypes survive in vaccum and suffer in high temperatures,
// so I'll just define this once, for both (see radiation comment above)
if(!environment) return
diff --git a/code/modules/mob/living/carbon/brain/life.dm b/code/modules/mob/living/carbon/brain/life.dm
index 011c4f405a5..266511428ae 100644
--- a/code/modules/mob/living/carbon/brain/life.dm
+++ b/code/modules/mob/living/carbon/brain/life.dm
@@ -35,6 +35,7 @@
/mob/living/carbon/brain/handle_environment(datum/gas_mixture/environment)
+ ..()
if(!environment)
return
var/environment_heat_capacity = environment.heat_capacity()
diff --git a/code/modules/mob/living/carbon/human/life.dm b/code/modules/mob/living/carbon/human/life.dm
index a8ed6a9f399..ba20691a832 100644
--- a/code/modules/mob/living/carbon/human/life.dm
+++ b/code/modules/mob/living/carbon/human/life.dm
@@ -303,6 +303,8 @@
return breath
/mob/living/carbon/human/handle_environment(datum/gas_mixture/environment)
+ ..()
+
if(!environment)
return
diff --git a/code/modules/mob/living/carbon/slime/life.dm b/code/modules/mob/living/carbon/slime/life.dm
index 62b27fa611b..b288e76ff00 100644
--- a/code/modules/mob/living/carbon/slime/life.dm
+++ b/code/modules/mob/living/carbon/slime/life.dm
@@ -16,6 +16,7 @@
handle_speech_and_mood()
/mob/living/carbon/slime/handle_environment(datum/gas_mixture/environment)
+ ..()
if(!environment)
adjustToxLoss(rand(10,20))
return
diff --git a/code/modules/mob/living/life.dm b/code/modules/mob/living/life.dm
index 1d7bd462326..759b99ac6ce 100644
--- a/code/modules/mob/living/life.dm
+++ b/code/modules/mob/living/life.dm
@@ -10,10 +10,10 @@
if(!loc)
return
- var/datum/gas_mixture/environment = loc.return_air()
+ var/datum/gas_mixture/gas_environment = loc.return_air()
//Handle temperature/pressure differences between body and environment
- if(environment)
- handle_environment(environment)
+ if(gas_environment)
+ handle_environment(gas_environment)
blinded = 0 // Placing this here just show how out of place it is.
@@ -57,9 +57,6 @@
/mob/living/proc/handle_random_events()
return
-/mob/living/proc/handle_environment(var/datum/gas_mixture/environment)
- return
-
/mob/living/proc/update_pulling()
if(pulling)
if(incapacitated())
@@ -189,3 +186,57 @@
/mob/living/proc/handle_hud_icons_health()
return
+
+/mob/living
+ var/datum/weakref/last_weather
+
+/mob/living/proc/is_outside()
+ var/turf/T = loc
+ return istype(T) && T.is_outside()
+
+/mob/living/proc/get_affecting_weather()
+ var/turf/my_turf = get_turf(src)
+ if(!istype(my_turf))
+ return
+ var/turf/actual_loc = loc
+ // If we're standing in the rain, use the turf weather.
+ . = istype(actual_loc) && actual_loc.weather
+ if(!.) // If we're under or inside shelter, use the z-level rain (for ambience)
+ . = SSweather.weather_by_z["[my_turf.z]"]
+
+/mob/living/proc/handle_environment(var/datum/gas_mixture/environment)
+
+ SHOULD_CALL_PARENT(TRUE)
+
+ // Handle physical effects of weather.
+ var/singleton/state/weather/weather_state
+ var/obj/abstract/weather_system/weather = get_affecting_weather()
+ if(weather)
+ weather_state = weather.weather_system.current_state
+ if(istype(weather_state))
+ weather_state.handle_exposure(src, get_weather_exposure(weather), weather)
+
+ // Refresh weather ambience.
+ // Show messages and play ambience.
+ if(client)
+
+ // Work out if we need to change or cancel the current ambience sound.
+ var/send_sound
+ var/mob_ref = WEAKREF(src)
+ if(istype(weather_state))
+ var/ambient_sounds = !is_outside() ? weather_state.ambient_indoors_sounds : weather_state.ambient_sounds
+ var/ambient_sound = length(ambient_sounds) && pick(ambient_sounds)
+ if(GLOB.current_mob_ambience[mob_ref] == ambient_sound)
+ return
+ send_sound = ambient_sound
+ GLOB.current_mob_ambience[mob_ref] = send_sound
+ else if(mob_ref in GLOB.current_mob_ambience)
+ GLOB.current_mob_ambience -= mob_ref
+ else
+ return
+
+ // Push sound to client. Pipe dream TODO: crossfade between the new and old weather ambience.
+ sound_to(src, sound(null, repeat = 0, wait = 0, volume = 0, channel = sound_channels.weather_channel))
+ if(send_sound)
+ sound_to(src, sound(send_sound, repeat = TRUE, wait = 0, volume = 30, channel = sound_channels.weather_channel))
+
diff --git a/code/modules/mob/living/login.dm b/code/modules/mob/living/login.dm
index 682e7ec19bf..8c208a0818a 100644
--- a/code/modules/mob/living/login.dm
+++ b/code/modules/mob/living/login.dm
@@ -7,4 +7,13 @@
//If they're SSD, remove it so they can wake back up.
update_antag_icons(mind)
reset_death_timers()
+
+ // Clear our cosmetic/sound weather cooldowns.
+ var/obj/abstract/weather_system/weather = get_affecting_weather()
+
+ var/mob_ref = WEAKREF(src)
+ if(istype(weather))
+ weather.mob_shown_weather -= mob_ref
+ weather.mob_shown_wind -= mob_ref
+ GLOB.current_mob_ambience -= mob_ref
return .
diff --git a/code/modules/mob/mob.dm b/code/modules/mob/mob.dm
index 0844326fb92..254660f4192 100644
--- a/code/modules/mob/mob.dm
+++ b/code/modules/mob/mob.dm
@@ -1395,6 +1395,58 @@
var/list/data = list()
return data
+/mob/proc/get_weather_protection()
+ for(var/obj/item/brolly in get_active_hand())
+ if(brolly.gives_weather_protection())
+ LAZYADD(., brolly)
+ if(!LAZYLEN(.))
+ for(var/turf/T as anything in RANGE_TURFS(1, loc))
+ for(var/obj/structure/flora/tree in T)
+ if(tree.protects_against_weather)
+ LAZYADD(., tree)
+
+/mob/living/carbon/human/get_weather_protection()
+ . = ..()
+ if(!LAZYLEN(.))
+ var/obj/item/clothing/head/check_head = get_equipped_item(slot_head_str)
+ if(!istype(check_head) || !check_head.protects_against_weather)
+ return
+ var/obj/item/clothing/suit/check_body = get_equipped_item(slot_wear_suit_str)
+ if(!istype(check_body) || !check_body.protects_against_weather)
+ return
+ LAZYADD(., check_head)
+ LAZYADD(., check_body)
+
+/mob/proc/get_weather_exposure()
+
+ // We're inside something else.
+ if(!isturf(loc))
+ return WEATHER_IGNORE
+
+ var/turf/T = loc
+ // We're under a roof or otherwise shouldn't be being rained on.
+ if(!T.is_outside())
+
+ // For non-multiz we'll give everyone some nice ambience.
+ if(!HasAbove(T.z))
+ return WEATHER_ROOFED
+
+ // For multi-z, check the actual weather on the turf above.
+ // TODO: maybe make this a property of the z-level marker.
+ var/turf/above = GetAbove(T)
+ if(above.weather)
+ return WEATHER_ROOFED
+
+ // Being more than one level down should exempt us from ambience.
+ return WEATHER_IGNORE
+
+ // Nothing's protecting us from the rain here
+ var/list/weather_protection = get_weather_protection()
+ if(LAZYLEN(weather_protection))
+ return WEATHER_PROTECTED
+
+ return WEATHER_EXPOSED
+
#undef UNBUCKLED
#undef PARTIALLY_BUCKLED
#undef FULLY_BUCKLED
diff --git a/code/modules/overmap/exoplanets/decor/_areas.dm b/code/modules/overmap/exoplanets/decor/_areas.dm
index 16788afce4c..c9abfa98c46 100644
--- a/code/modules/overmap/exoplanets/decor/_areas.dm
+++ b/code/modules/overmap/exoplanets/decor/_areas.dm
@@ -3,6 +3,7 @@
ambience = list('sound/effects/wind/wind_2_1.ogg','sound/effects/wind/wind_2_2.ogg','sound/effects/wind/wind_3_1.ogg','sound/effects/wind/wind_4_1.ogg','sound/effects/wind/wind_4_2.ogg','sound/effects/wind/wind_5_1.ogg')
always_unpowered = 1
area_flags = AREA_FLAG_INDESTRUCTIBLE_TURFS
+ is_outside = OUTSIDE_YES
/area/exoplanet/adhomai
name = "Adhomian Wilderness"
diff --git a/code/modules/overmap/exoplanets/decor/_flora.dm b/code/modules/overmap/exoplanets/decor/_flora.dm
index 99e485dba20..ea96b9e631d 100644
--- a/code/modules/overmap/exoplanets/decor/_flora.dm
+++ b/code/modules/overmap/exoplanets/decor/_flora.dm
@@ -4,6 +4,8 @@
anchored = TRUE
density = TRUE
var/cutting
+ /// Whether or not you can shelter under this.
+ var/protects_against_weather = FALSE
/obj/structure/flora/proc/dig_up(mob/user)
user.visible_message(SPAN_NOTICE("\The [user] begins digging up \the [src]..."))
diff --git a/code/modules/overmap/exoplanets/decor/flora/_tree.dm b/code/modules/overmap/exoplanets/decor/flora/_tree.dm
index 2cf7b645ba1..fe24e8f7855 100644
--- a/code/modules/overmap/exoplanets/decor/flora/_tree.dm
+++ b/code/modules/overmap/exoplanets/decor/flora/_tree.dm
@@ -4,6 +4,7 @@
density = TRUE
layer = 9
pixel_x = -16
+ protects_against_weather = TRUE
var/max_chop_health = 180
var/chop_health = 180 //15 hits with steel hatchet, 5 with wielded fireaxe
var/fall_force = 60
diff --git a/code/modules/overmap/exoplanets/exoplanet.dm b/code/modules/overmap/exoplanets/exoplanet.dm
index 5883933fea9..c5bee382626 100644
--- a/code/modules/overmap/exoplanets/exoplanet.dm
+++ b/code/modules/overmap/exoplanets/exoplanet.dm
@@ -54,6 +54,9 @@
var/list/possible_themes = list(/datum/exoplanet_theme)
var/datum/exoplanet_theme/theme
+ ///What weather state to use for this planet initially. If null, will not initialize any weather system. Must be a typepath rather than an instance.
+ var/singleton/state/weather/initial_weather_state = /singleton/state/weather/calm
+
var/features_budget = 4
var/list/possible_features = list()
var/list/spawned_features
@@ -197,6 +200,8 @@
/obj/effect/overmap/visitable/sector/exoplanet/proc/build_level()
generate_habitability()
generate_atmosphere()
+ if(ispath(initial_weather_state))
+ generate_weather()
generate_flora()
generate_map()
generate_features()
@@ -533,3 +538,17 @@
/obj/effect/landmark/exoplanet_spawn/proc/do_spawn(obj/effect/overmap/visitable/sector/exoplanet/planet)
return
+
+///Resets the given weather state to our planet replacing the old one, and trigger updates. Can be a type path or instance.
+/obj/effect/overmap/visitable/sector/exoplanet/proc/reset_weather(var/singleton/state/weather/W)
+ initial_weather_state = W
+ if(!(z in map_z))
+ return //It's entire possible the levels weren't initialized yet, so don't bother.
+ //Tells all our levels exposed to the sky to force change the weather.
+ SSweather.setup_weather_system(z, initial_weather_state)
+
+///Setup the initial weather state for the planet. Doesn't apply it to our z levels however.
+/obj/effect/overmap/visitable/sector/exoplanet/proc/generate_weather()
+ if(ispath(initial_weather_state))
+ initial_weather_state = GET_SINGLETON(initial_weather_state)
+ reset_weather(initial_weather_state)
diff --git a/code/modules/tgui_input/say_modal/typing.dm b/code/modules/tgui_input/say_modal/typing.dm
index b6b1711fd78..e1ed5e194db 100644
--- a/code/modules/tgui_input/say_modal/typing.dm
+++ b/code/modules/tgui_input/say_modal/typing.dm
@@ -114,7 +114,7 @@ I IS TYPIN'!'
/atom/movable/typing_indicator/Destroy()
if(master)
- master.vis_contents -= src
+ master.remove_vis_contents(src)
if(ismob(master))
var/mob/owner = master
if(owner.typing_indicator == src)
@@ -131,7 +131,7 @@ I IS TYPIN'!'
set_invisibility(INVISIBILITY_MAXIMUM)
if(ismovable(master))
var/atom/movable/owner = master
- owner.vis_contents -= src
+ owner.remove_vis_contents(src)
shown = FALSE
/atom/movable/typing_indicator/proc/show_typing_indicator(var/thinking = FALSE)
@@ -149,7 +149,7 @@ I IS TYPIN'!'
if(ismovable(master))
var/atom/movable/owner = master
- owner.vis_contents += src
+ owner.add_vis_contents(src)
if(!shown)
// Animate it popping up from nowhere.
diff --git a/code/modules/weather/_weather.dm b/code/modules/weather/_weather.dm
new file mode 100644
index 00000000000..098e7a0a2cc
--- /dev/null
+++ b/code/modules/weather/_weather.dm
@@ -0,0 +1,99 @@
+/*
+ * Notes on weather:
+ *
+ * - Weather is a single object that sits in the vis_contents of all outside turfs on
+ * its associated z-levels and is removed or added by /turf/proc/update_weather(),
+ * which is usually called from /turf/proc/set_outside().
+ *
+ * - Weather generally assumes any atom that cares about it will ask it directly and
+ * mobs do this in /mob/living/proc/handle_environment().
+ *
+ * - For this system to be scalable, it should minimize the amount of list-based
+ * processing it does and be primarily passive, allowing mobs to ignore it or
+ * poll it on their own time.
+ *
+ * - The weather object is queued on SSweather and is polled every fifteen seconds at time
+ * of writing. This is handled in /obj/abstract/weather_system/proc/tick().
+ *
+ * - When evaluating, weather will generally get more intense or more severe rather than
+ * jumping around randomly. Each state will set a minimum duration based on min/max time.
+ *
+ * - If polled between weather updates there is a chance of modifying wind speed and direction
+ * instead.
+ */
+
+/obj/abstract/weather_system
+ plane = PLANE_DEFAULT
+ layer = ABOVE_ALL_MOB_LAYER
+ icon = 'icons/effects/weather.dmi'
+ icon_state = "blank"
+ invisibility = 0
+ appearance_flags = (RESET_COLOR | RESET_ALPHA | RESET_TRANSFORM)
+
+ var/water_material = null // Material to use for the properties of rain.
+ var/ice_material = null // Material to use for the properties of snow and hail.
+
+ var/list/affecting_zs // What z-levels are we affecting?
+ var/datum/state_machine/weather/weather_system // What is our internal state and how do we decide what state to use?
+ var/next_weather_transition = 0 // What world.time will we next evaluate our state?
+
+ var/obj/abstract/lightning_overlay/lightning_overlay // A visible atom used for animated lighting effects.
+ var/tmp/list/vis_contents_additions // Holder for a list used to add required atoms to turf vis_contents.
+
+// Main heartbeat proc, called by SSweather.
+/obj/abstract/weather_system/proc/tick()
+
+ // Check if we should move to a new state.
+ if(world.time >= next_weather_transition)
+ weather_system.evaluate()
+
+ // Change wind direction and speed.
+ handle_wind()
+
+ // Handle periodic effects for ticks (like lightning)
+ var/singleton/state/weather/rain/weather_state = weather_system.current_state
+ if(istype(weather_state))
+ weather_state.tick(src)
+
+
+/obj/abstract/weather_system/Destroy()
+ // Clean ourselves out of the vis_contents of our affected turfs.
+ for(var/tz in affecting_zs)
+ for(var/turf/T as anything in block(locate(1, 1, tz), locate(world.maxx, world.maxy, tz)))
+ if(T.weather == src)
+ T.remove_vis_contents(vis_contents_additions)
+ T.weather = null
+ vis_contents_additions.Cut()
+ SSweather.unregister_weather_system(src)
+ QDEL_NULL(lightning_overlay)
+ . = ..()
+
+// Called by /turf/examine() to show current weather status.
+/obj/abstract/weather_system/examine(mob/user, distance)
+ SHOULD_CALL_PARENT(FALSE)
+ var/singleton/state/weather/weather_state = weather_system.current_state
+ if(istype(weather_state))
+ to_chat(user, weather_state.descriptor)
+ show_wind(user, force = TRUE)
+
+// Called by /singleton/state/weather to assess validity of a state in the weather FSM.
+/obj/abstract/weather_system/proc/supports_weather_state(var/singleton/state/weather/next_state)
+ // Exoplanet stuff for the future:
+ // - TODO: track and check exoplanet temperature.
+ // - TODO: compare to a list of 'acceptable' states
+ if(istype(next_state))
+ if(next_state.is_liquid)
+ return !!water_material
+ if(next_state.is_ice)
+ return !!ice_material
+ return TRUE
+ return FALSE
+
+// Dummy object for lightning flash animation.
+/obj/abstract/lightning_overlay
+ plane = PLANE_DEFAULT + 1
+ layer = EFFECTS_ABOVE_LIGHTING_LAYER
+ icon = 'icons/effects/weather.dmi'
+ icon_state = "full"
+ alpha = 0
+ invisibility = 0
diff --git a/code/modules/weather/weather_debug.dm b/code/modules/weather/weather_debug.dm
new file mode 100644
index 00000000000..45a521051ec
--- /dev/null
+++ b/code/modules/weather/weather_debug.dm
@@ -0,0 +1,67 @@
+/datum/admins/proc/force_kill_weather()
+ set name = "Kill Weather For Level"
+ set desc = "Destroys a weather system for a level if present."
+ set category = "Debug"
+
+ if(!check_rights(R_DEBUG))
+ return
+
+ var/turf/T = get_turf(usr)
+ if(!istype(T))
+ to_chat(usr, SPAN_WARNING("You need to have a turf to use this verb."))
+ return
+
+ var/obj/abstract/weather_system/weather = T.weather || SSweather.weather_by_z["[T.z]"]
+ if(!weather)
+ to_chat(usr, SPAN_WARNING("This z-level does not have weather."))
+ return
+
+ qdel(weather)
+ to_chat(usr, SPAN_NOTICE("Weather destroyed for z[T.z]."))
+
+/datum/admins/proc/force_initialize_weather()
+ set name = "Initialize Weather For Level"
+ set desc = "Creates a weather system for a level if not present."
+ set category = "Debug"
+
+ if(!check_rights(R_DEBUG))
+ return
+
+ var/turf/T = get_turf(usr)
+ while(T && HasAbove(T.z))
+ T = GetAbove(T)
+ if(!istype(T))
+ to_chat(usr, SPAN_WARNING("You need to have a turf to use this verb."))
+ return
+
+ var/obj/abstract/weather_system/weather = T.weather || SSweather.weather_by_z["[T.z]"]
+ if(weather)
+ to_chat(usr, SPAN_WARNING("This z-level already has weather."))
+ return
+ SSweather.setup_weather_system(T.z)
+ to_chat(usr, SPAN_NOTICE("Weather created for z[T.z]."))
+
+/datum/admins/proc/force_weather_state()
+ set name = "Force Weather State"
+ set desc = "Force the local weather to use a given state."
+ set category = "Debug"
+
+ if(!check_rights(R_DEBUG))
+ return
+
+ var/turf/T = get_turf(usr)
+ if(!istype(T))
+ to_chat(usr, SPAN_WARNING("You need to have a turf to use this verb."))
+ return
+
+ var/obj/abstract/weather_system/weather = T.weather || SSweather.weather_by_z["[T.z]"]
+ if(!weather)
+ to_chat(usr, SPAN_WARNING("This z-level has no weather. Use Initialize Weather For Level if you want to create it."))
+ return
+
+ var/use_state = input(usr, "Which state do you wish to use?", "Target State") as null|anything in GET_SINGLETON_SUBTYPE_LIST(/singleton/state/weather)
+ if(!use_state || weather != (T.weather || SSweather.weather_by_z["[T.z]"]))
+ return
+ weather.weather_system.set_state(use_state)
+ var/singleton/state/weather/weather_state = GET_SINGLETON(use_state)
+ to_chat(usr, SPAN_NOTICE("Set weather for z[T.z] to [weather_state.name]."))
diff --git a/code/modules/weather/weather_effects.dm b/code/modules/weather/weather_effects.dm
new file mode 100644
index 00000000000..8261c3dc5c8
--- /dev/null
+++ b/code/modules/weather/weather_effects.dm
@@ -0,0 +1,48 @@
+/obj/abstract/weather_system/proc/get_movement_delay(var/datum/gas_mixture/env, var/travel_dir)
+
+ // It's quiet. Too quiet.
+ if(!wind_direction || !base_wind_delay || !travel_dir || !env || env.return_pressure() < MIN_WIND_PRESSURE)
+ return 0
+
+ // May the wind be always at your back!
+ var/current_wind_strength = round(wind_strength * base_wind_delay)
+ if(wind_direction == travel_dir)
+ return -(current_wind_strength)
+ if(travel_dir & wind_direction)
+ return -(round(current_wind_strength * 0.5))
+
+ // Never spit into the wind.
+ var/reversed_wind = GLOB.reverse_dir[wind_direction]
+ if(reversed_wind == travel_dir)
+ return current_wind_strength
+ if(travel_dir & reversed_wind)
+ return round(current_wind_strength * 0.5)
+
+ return 0
+
+/obj/abstract/weather_system/proc/adjust_temperature(initial_temperature)
+ return initial_temperature
+
+/obj/abstract/weather_system/proc/show_weather(var/mob/M)
+ var/mob_ref = WEAKREF(M)
+ if(mob_shown_weather[mob_ref])
+ return FALSE
+ var/singleton/state/weather/current_weather = weather_system?.current_state
+ if(!istype(current_weather))
+ return FALSE
+ mob_shown_weather[mob_ref] = TRUE
+ current_weather.show_to(M, src)
+ return TRUE
+
+/obj/abstract/weather_system/proc/lightning_strike()
+ set waitfor = FALSE
+ animate(lightning_overlay, alpha = 255, time = 2)
+ for(var/client/C)
+ if(!isliving(C.mob))
+ continue
+ var/turf/T = get_turf(C.mob)
+ if(!(T.z in affecting_zs))
+ continue
+ sound_to(C.mob, sound('sound/effects/weather/thunder.ogg', repeat = FALSE, wait = 0, volume = 100))
+ sleep(3)
+ animate(lightning_overlay, alpha = initial(lightning_overlay.alpha), time = 5 SECONDS)
diff --git a/code/modules/weather/weather_fsm.dm b/code/modules/weather/weather_fsm.dm
new file mode 100644
index 00000000000..3f8d3e47117
--- /dev/null
+++ b/code/modules/weather/weather_fsm.dm
@@ -0,0 +1,10 @@
+/datum/state_machine/weather
+ expected_type = /obj/abstract/weather_system
+
+/datum/state_machine/weather/choose_transition(list/valid_transitions)
+ var/list/transitions = list()
+ for(var/singleton/state_transition/weather/state_transition in valid_transitions)
+ transitions[state_transition] = state_transition.likelihood_weighting
+ if(length(transitions))
+ return pick(transitions) //pickweight(transitions)
+ return ..()
diff --git a/code/modules/weather/weather_fsm_state_transitions.dm b/code/modules/weather/weather_fsm_state_transitions.dm
new file mode 100644
index 00000000000..14ac0ad8b15
--- /dev/null
+++ b/code/modules/weather/weather_fsm_state_transitions.dm
@@ -0,0 +1,31 @@
+/singleton/state_transition/weather
+ abstract_type = /singleton/state_transition/weather
+ var/likelihood_weighting = 100
+
+/singleton/state_transition/weather/is_open(datum/holder)
+ var/obj/abstract/weather_system/weather = holder
+ return weather.supports_weather_state(target)
+
+/singleton/state_transition/weather/calm
+ target = /singleton/state/weather/calm
+ likelihood_weighting = 50
+
+/singleton/state_transition/weather/snow
+ target = /singleton/state/weather/snow
+
+/singleton/state_transition/weather/rain
+ target = /singleton/state/weather/rain
+
+/singleton/state_transition/weather/snow_medium
+ target = /singleton/state/weather/snow/medium
+
+/singleton/state_transition/weather/snow_heavy
+ target = /singleton/state/weather/snow/heavy
+ likelihood_weighting = 20
+
+/singleton/state_transition/weather/storm
+ target = /singleton/state/weather/rain/storm
+
+/singleton/state_transition/weather/hail
+ target = /singleton/state/weather/rain/hail
+ likelihood_weighting = 20
diff --git a/code/modules/weather/weather_fsm_states.dm b/code/modules/weather/weather_fsm_states.dm
new file mode 100644
index 00000000000..2ee5ffc177f
--- /dev/null
+++ b/code/modules/weather/weather_fsm_states.dm
@@ -0,0 +1,191 @@
+/singleton/state/weather
+
+ abstract_type = /singleton/state/weather
+
+ var/name = "Undefined"
+ var/descriptor = "The weather is undefined."
+
+ var/cosmetic_message_chance = 5
+ var/list/cosmetic_messages
+ var/list/protected_messages
+ var/cosmetic_span_class = "notice"
+
+ var/icon = 'icons/effects/weather.dmi'
+ var/icon_state
+
+ var/alpha = 210
+ var/minimum_time = 2 MINUTES
+ var/maximum_time = 10 MINUTES
+ var/is_liquid = FALSE
+ var/is_ice = FALSE
+
+ var/list/ambient_sounds
+ var/list/ambient_indoors_sounds
+
+/singleton/state/weather/entered_state(datum/holder)
+ . = ..()
+
+ var/obj/abstract/weather_system/weather = holder
+ weather.next_weather_transition = world.time + rand(minimum_time, maximum_time)
+ weather.mob_shown_weather.Cut()
+ weather.icon = icon
+ weather.icon_state = icon_state
+ weather.alpha = alpha
+
+ if(is_liquid && weather.water_material)
+ var/material/mat = SSmaterials.get_material_by_name(weather.water_material)
+ weather.color = mat.icon_colour
+ else if(is_ice && weather.ice_material)
+ var/material/mat = SSmaterials.get_material_by_name(weather.ice_material)
+ weather.color = mat.icon_colour
+ else
+ weather.color = COLOR_WHITE
+
+/singleton/state/weather/proc/tick(var/obj/abstract/weather_system/weather)
+ return
+
+/singleton/state/weather/proc/handle_roofed_effects(var/mob/living/M, var/obj/abstract/weather_system/weather)
+ return
+
+/singleton/state/weather/proc/handle_protected_effects(var/mob/living/M, var/obj/abstract/weather_system/weather, var/obj/item/protected_by)
+ if(prob(cosmetic_message_chance))
+ if(protected_by && length(protected_messages))
+ if(protected_by.loc == M)
+ to_chat(M, "[replacetext(pick(protected_messages), "$ITEM$", "your [protected_by.name]")]")
+ else
+ to_chat(M, "[replacetext(pick(protected_messages), "$ITEM$", "\the [protected_by]")]")
+ else if(length(cosmetic_messages))
+ to_chat(M, "[pick(cosmetic_messages)]")
+
+/singleton/state/weather/proc/handle_exposure_effects(var/mob/living/M, var/obj/abstract/weather_system/weather)
+ handle_protected_effects(M, weather)
+
+/singleton/state/weather/proc/handle_exposure(var/mob/living/M, var/exposure, var/obj/abstract/weather_system/weather)
+
+ // Send strings if we're outside.
+ if(M.is_outside() && M.client)
+ if(!weather.show_weather(M))
+ weather.show_wind(M)
+
+ if(exposure != WEATHER_IGNORE && weather.set_cooldown(M))
+ if(exposure == WEATHER_EXPOSED)
+ handle_exposure_effects(M, weather)
+ else if(exposure == WEATHER_ROOFED)
+ handle_roofed_effects(M, weather)
+ else if(exposure == WEATHER_PROTECTED)
+ var/list/protected_by = M.get_weather_protection()
+ if(LAZYLEN(protected_by))
+ handle_protected_effects(M, weather, pick(protected_by))
+
+/singleton/state/weather/proc/show_to(var/mob/living/M, var/obj/abstract/weather_system/weather)
+ to_chat(M, descriptor)
+
+/singleton/state/weather/calm
+ name = "Calm"
+ icon_state = "blank"
+ descriptor = "The weather is calm."
+ transitions = list(
+ /singleton/state_transition/weather/snow,
+ /singleton/state_transition/weather/rain
+ )
+
+/singleton/state/weather/snow
+ name = "Light Snow"
+ icon_state = "snowfall_light"
+ descriptor = "It is snowing gently."
+ is_ice = TRUE
+ transitions = list(
+ /singleton/state_transition/weather/calm,
+ /singleton/state_transition/weather/rain,
+ /singleton/state_transition/weather/snow_medium
+ )
+ ambient_sounds = list('sound/effects/weather/snow.ogg')
+ protected_messages = list("Snowflakes collect atop $ITEM$.")
+ cosmetic_messages = list(
+ "Snowflakes fall slowly around you.",
+ "Flakes of snow drift gently past."
+ )
+
+/singleton/state/weather/snow/medium
+ name = "Snow"
+ icon_state = "snowfall_med"
+ descriptor = "It is snowing."
+ transitions = list(
+ /singleton/state_transition/weather/snow,
+ /singleton/state_transition/weather/snow_heavy
+ )
+
+/singleton/state/weather/snow/heavy
+ name = "Heavy Snow"
+ icon_state = "snowfall_heavy"
+ descriptor = "It is snowing heavily."
+ transitions = list(/singleton/state_transition/weather/snow_medium)
+ cosmetic_messages = list(
+ "Gusting snow obscures your vision.",
+ "Thick flurries of snow swirl around you."
+ )
+ cosmetic_span_class = "warning"
+
+/singleton/state/weather/rain
+ name = "Light Rain"
+ icon_state = "rain"
+ descriptor = "It is raining gently."
+ cosmetic_span_class = "notice"
+ is_liquid = TRUE
+ transitions = list(
+ /singleton/state_transition/weather/calm,
+ /singleton/state_transition/weather/storm
+ )
+ ambient_sounds = list('sound/effects/weather/rain.ogg')
+ ambient_indoors_sounds = list('sound/effects/weather/rain_indoors.ogg')
+ cosmetic_messages = list("Raindrops patter against you.")
+ protected_messages = list("Raindrops patter against $ITEM$.")
+ var/list/roof_messages = list("Rain patters against the roof.")
+
+/singleton/state/weather/rain/handle_roofed_effects(var/mob/living/M, var/obj/abstract/weather_system/weather)
+ if(length(roof_messages) && prob(cosmetic_message_chance))
+ to_chat(M, "[pick(roof_messages)]")
+
+/singleton/state/weather/rain/storm
+ name = "Heavy Rain"
+ icon_state = "storm"
+ descriptor = "It is raining heavily."
+ cosmetic_span_class = "warning"
+ transitions = list(
+ /singleton/state_transition/weather/rain,
+ /singleton/state_transition/weather/hail
+ )
+ cosmetic_messages = list("Torrential rain thunders down around you.")
+ protected_messages = list("Torrential rain thunders against $ITEM$.")
+ roof_messages = list("Torrential rain thunders against the roof.")
+ ambient_sounds = list('sound/effects/weather/rain_heavy.ogg')
+
+/singleton/state/weather/rain/storm/tick(var/obj/abstract/weather_system/weather)
+ ..()
+ if(prob(0.5))
+ weather.lightning_strike()
+
+/singleton/state/weather/rain/hail
+ name = "Hail"
+ icon_state = "hail"
+ descriptor = "It is hailing."
+ cosmetic_span_class = "danger"
+ is_liquid = FALSE
+ is_ice = TRUE
+ transitions = list(/singleton/state_transition/weather/storm)
+ cosmetic_messages = list("Hail patters around you.")
+ protected_messages = list("Hail patters against $ITEM$.")
+ roof_messages = list("Hail clatters on the roof.")
+ ambient_sounds = list('sound/effects/weather/rain.ogg')
+ ambient_indoors_sounds = list('sound/effects/weather/hail_indoors.ogg')
+
+/singleton/state/weather/rain/hail/handle_exposure_effects(var/mob/living/M, var/obj/abstract/weather_system/weather)
+ to_chat(M, SPAN_DANGER("You are pelted by a shower of hail!"))
+ M.adjustBruteLoss(rand(1,3))
+
+/singleton/state/weather/ash
+ name = "Ash"
+ icon_state = "ashfall_light"
+ descriptor = "A rain of ash falls from the sky."
+ cosmetic_span_class = "warning"
+ cosmetic_messages = list("Drifts of ash fall from the sky.")
diff --git a/code/modules/weather/weather_init.dm b/code/modules/weather/weather_init.dm
new file mode 100644
index 00000000000..fc34dff97bd
--- /dev/null
+++ b/code/modules/weather/weather_init.dm
@@ -0,0 +1,38 @@
+INITIALIZE_IMMEDIATE(/obj/abstract/weather_system)
+/obj/abstract/weather_system/Initialize(var/ml, var/target_z, var/initial_weather)
+ SSweather.register_weather_system(src)
+
+ . = ..()
+
+ set_invisibility(0)
+
+ // Bookkeeping/rightclick guards.
+ verbs.Cut()
+ forceMove(null)
+ lightning_overlay = new
+ vis_contents_additions = list(src, lightning_overlay)
+
+ // Initialize our state machine.
+ weather_system = add_state_machine(src, /datum/state_machine/weather)
+ weather_system.set_state(initial_weather || /singleton/state/weather/calm)
+
+ // Track our affected z-levels.
+ affecting_zs = GetConnectedZlevels(target_z)
+
+ // If we're post-init, init immediately.
+ if(SSweather.initialized)
+ addtimer(CALLBACK(src, PROC_REF(init_weather)), 0)
+
+// Start the weather effects from the highest point; they will propagate downwards during update.
+/obj/abstract/weather_system/proc/init_weather()
+ // Track all z-levels.
+ for(var/highest_z in affecting_zs)
+ var/turfcount = 0
+ if(HasAbove(highest_z))
+ continue
+ // Update turf weather.
+ for(var/turf/T as anything in block(locate(1, 1, highest_z), locate(world.maxx, world.maxy, highest_z)))
+ T.update_weather(src)
+ turfcount++
+ CHECK_TICK
+ log_debug("Initialized weather for [turfcount] turf\s from z[highest_z].")
diff --git a/code/modules/weather/weather_mob_tracking.dm b/code/modules/weather/weather_mob_tracking.dm
new file mode 100644
index 00000000000..d0e4a9cd43b
--- /dev/null
+++ b/code/modules/weather/weather_mob_tracking.dm
@@ -0,0 +1,23 @@
+
+GLOBAL_LIST_EMPTY(current_mob_ambience)
+/obj/abstract/weather_system
+
+ // Weakref lists used to track mobs within our weather
+ // system; alternative to keeping big lists of actual mobs or
+ // having mobs constantly poked by weather systems.
+
+ var/tmp/list/mobs_on_cooldown = list() // Has this mob recently been messed with by the weather?
+ var/tmp/list/mob_shown_weather = list() // Has this mob been sent the summary message about the current weather?
+ var/tmp/list/mob_shown_wind = list() // Has this mob been sent the summary message about the current wind?
+
+// 'cooldown' in this context refers to weather effects like hail damage or being shown cosmetic messages.
+/obj/abstract/weather_system/proc/set_cooldown(var/mob/living/M, var/delay = 5 SECONDS)
+ var/mobref = WEAKREF(M)
+ if(!(mobref in mobs_on_cooldown))
+ mobs_on_cooldown[mobref] = TRUE
+ addtimer(CALLBACK(src, PROC_REF(clear_cooldown), mobref), delay)
+ return TRUE
+ return FALSE
+
+/obj/abstract/weather_system/proc/clear_cooldown(var/mobref)
+ mobs_on_cooldown -= mobref
diff --git a/code/modules/weather/weather_wind.dm b/code/modules/weather/weather_wind.dm
new file mode 100644
index 00000000000..e4624673dc0
--- /dev/null
+++ b/code/modules/weather/weather_wind.dm
@@ -0,0 +1,38 @@
+/obj/abstract/weather_system
+ var/tmp/wind_direction = 0 // Bitflag; current wind direction.
+ var/tmp/wind_strength = 1 // How string is the wind currently?
+ var/const/base_wind_delay = 1 // What is the base movement delay or increase applied by wind strength?
+
+// Randomizes wind speed and direction sometimes.
+/obj/abstract/weather_system/proc/handle_wind()
+ // TODO: turbulence for chance of wind changes,
+ // ferocity for strength of wind changes
+ if(prob(66))
+ return
+ if(prob(10))
+ if(!wind_direction)
+ wind_direction = pick(GLOB.alldirs)
+ else
+ wind_direction = turn(wind_direction, pick(45, -45))
+ mob_shown_wind.Cut()
+ if(prob(10))
+ var/old_strength = wind_strength
+ wind_strength = clamp(wind_strength + rand(-1, 1), 5, -5)
+ if(old_strength != wind_strength)
+ mob_shown_wind.Cut()
+
+/obj/abstract/weather_system/proc/show_wind(var/mob/M, var/force = FALSE)
+ var/mob_ref = WEAKREF(M)
+ if(mob_shown_wind[mob_ref] && !force)
+ return FALSE
+ mob_shown_wind[mob_ref] = TRUE
+ . = TRUE
+ var/turf/T = get_turf(M)
+ if(istype(T))
+ var/datum/gas_mixture/environment = T.return_air()
+ if(environment && environment.return_pressure() >= MIN_WIND_PRESSURE) // Arbitrary low pressure bound.
+ var/absolute_strength = abs(wind_strength)
+ if(absolute_strength <= 0.5 || !wind_direction)
+ to_chat(M, SPAN_NOTICE("The wind is calm."))
+ else
+ to_chat(M, SPAN_NOTICE("The wind is blowing[absolute_strength > 2 ? " strongly" : ""] towards the [dir2text(wind_direction)]."))
diff --git a/html/changelogs/mattatlas-getyourumbrella.yml b/html/changelogs/mattatlas-getyourumbrella.yml
new file mode 100644
index 00000000000..690087792ab
--- /dev/null
+++ b/html/changelogs/mattatlas-getyourumbrella.yml
@@ -0,0 +1,42 @@
+################################
+# Example Changelog File
+#
+# Note: This file, and files beginning with ".", and files that don't end in ".yml" will not be read. If you change this file, you will look really dumb.
+#
+# Your changelog will be merged with a master changelog. (New stuff added only, and only on the date entry for the day it was merged.)
+# When it is, any changes listed below will disappear.
+#
+# Valid Prefixes:
+# bugfix
+# wip (For works in progress)
+# tweak
+# soundadd
+# sounddel
+# rscadd (general adding of nice things)
+# rscdel (general deleting of nice things)
+# imageadd
+# imagedel
+# maptweak
+# spellcheck (typo fixes)
+# experiment
+# balance
+# admin
+# backend
+# security
+# refactor
+#################################
+
+# Your name.
+author: MattAtlas (Porting), Loaf (Original Code), Lancer (Umbrella Sprites)
+
+# Optional: Remove this file after generating master changelog. Useful for PR changelogs that won't get used again.
+delete-after: True
+
+# Any changes you've made. See valid prefix list above.
+# INDENT WITH TWO SPACES. NOT TABS. SPACES.
+# SCREW THIS UP AND IT WON'T WORK.
+# Also, all entries are changed into a single [] after a master changelog generation. Just remove the brackets when you add new entries.
+# Please surround your changes in double quotes ("), as certain characters otherwise screws up compiling. The quotes will not show up in the changelog.
+changes:
+ - rscadd: "Ported a weather system from Nebula. Original code credit is to Loaf."
+ - rscadd: "Added umbrellas to the game. Sprites by Lancer."
diff --git a/icons/effects/weather.dmi b/icons/effects/weather.dmi
new file mode 100644
index 00000000000..01aa9d456f4
Binary files /dev/null and b/icons/effects/weather.dmi differ
diff --git a/icons/obj/item/umbrellas.dmi b/icons/obj/item/umbrellas.dmi
new file mode 100644
index 00000000000..f7df1b524e7
Binary files /dev/null and b/icons/obj/item/umbrellas.dmi differ
diff --git a/maps/_common/areas/areas.dm b/maps/_common/areas/areas.dm
index 4507f02ba24..0381220c856 100644
--- a/maps/_common/areas/areas.dm
+++ b/maps/_common/areas/areas.dm
@@ -37,6 +37,7 @@ Generally you don't want to put your areas in here; if the area is only used in
ambience = AMBIENCE_SPACE
no_light_control = 1
base_turf = /turf/space
+ is_outside = OUTSIDE_YES
/area/space/atmosalert()
return
diff --git a/maps/away/away_site/konyang/point_verdant/point_verdant_areas.dm b/maps/away/away_site/konyang/point_verdant/point_verdant_areas.dm
index bc70dc52863..fc8ad61aab9 100644
--- a/maps/away/away_site/konyang/point_verdant/point_verdant_areas.dm
+++ b/maps/away/away_site/konyang/point_verdant/point_verdant_areas.dm
@@ -6,6 +6,7 @@
base_turf = /turf/simulated/floor/exoplanet/dirt_konyang
ambience = AMBIENCE_KONYANG_TRAFFIC
sound_env = CITY
+ is_outside = TRUE
var/lighting = FALSE //Is this area automatically lit?
/area/point_verdant/Initialize()
@@ -42,6 +43,7 @@
name = "Point Verdant - Indoors"
sound_env = LARGE_SOFTFLOOR
ambience = AMBIENCE_KONYANG_RAIN_MUFFLED
+ is_outside = FALSE
//Main city buildings
/area/point_verdant/interior/laundromat
diff --git a/sound/effects/weather/hail_indoors.ogg b/sound/effects/weather/hail_indoors.ogg
new file mode 100644
index 00000000000..e0158fde3a2
Binary files /dev/null and b/sound/effects/weather/hail_indoors.ogg differ
diff --git a/sound/effects/weather/rain.ogg b/sound/effects/weather/rain.ogg
new file mode 100644
index 00000000000..f65ba956f84
Binary files /dev/null and b/sound/effects/weather/rain.ogg differ
diff --git a/sound/effects/weather/rain_heavy.ogg b/sound/effects/weather/rain_heavy.ogg
new file mode 100644
index 00000000000..1a51c611c4f
Binary files /dev/null and b/sound/effects/weather/rain_heavy.ogg differ
diff --git a/sound/effects/weather/rain_indoors.ogg b/sound/effects/weather/rain_indoors.ogg
new file mode 100644
index 00000000000..9fdabfe558e
Binary files /dev/null and b/sound/effects/weather/rain_indoors.ogg differ
diff --git a/sound/effects/weather/snow.ogg b/sound/effects/weather/snow.ogg
new file mode 100644
index 00000000000..641d31596a6
Binary files /dev/null and b/sound/effects/weather/snow.ogg differ
diff --git a/sound/effects/weather/thunder.ogg b/sound/effects/weather/thunder.ogg
new file mode 100644
index 00000000000..2719fcc383e
Binary files /dev/null and b/sound/effects/weather/thunder.ogg differ