diff --git a/code/controllers/master_controller.dm b/code/controllers/master_controller.dm
index c2ba3db8151..8cafa13345a 100644
--- a/code/controllers/master_controller.dm
+++ b/code/controllers/master_controller.dm
@@ -21,6 +21,7 @@ datum/controller/game_controller
var/objects_cost = 0
var/networks_cost = 0
var/powernets_cost = 0
+ var/events_cost = 0
var/ticker_cost = 0
var/total_cost = 0
@@ -218,6 +219,20 @@ datum/controller/game_controller/proc/process()
sleep(breather_ticks)
+ //EVENTS
+ timer = world.timeofday
+ last_thing_processed = /datum/event
+ i = 1
+ while(i<=events.len)
+ var/datum/event/Event = events[i]
+ if(Event)
+ Event.process()
+ i++
+ continue
+ events.Cut(i,i+1)
+ checkEvent()
+ events_cost = (world.timeofday - timer) / 10
+
//TICKER
timer = world.timeofday
last_thing_processed = ticker.type
@@ -225,7 +240,7 @@ datum/controller/game_controller/proc/process()
ticker_cost = (world.timeofday - timer) / 10
//TIMING
- total_cost = air_cost + sun_cost + mobs_cost + diseases_cost + machines_cost + objects_cost + networks_cost + powernets_cost + ticker_cost
+ total_cost = air_cost + sun_cost + mobs_cost + diseases_cost + machines_cost + objects_cost + networks_cost + powernets_cost + events_cost + ticker_cost
var/end_time = world.timeofday
if(end_time < start_time)
diff --git a/code/game/gamemodes/gameticker.dm b/code/game/gamemodes/gameticker.dm
index 4183ac322da..df1674f3152 100644
--- a/code/game/gamemodes/gameticker.dm
+++ b/code/game/gamemodes/gameticker.dm
@@ -122,7 +122,8 @@ var/global/datum/controller/gameticker/ticker
//Holiday Round-start stuff ~Carn
Holiday_Game_Start()
- start_events() //handles random events and space dust.
+// start_events() //handles random events and space dust.
+//new random event system is handled from the MC.
var/admins_number = 0
for(var/client/C)
diff --git a/code/global.dm b/code/global.dm
index cd9570b5d87..e887fea82f0 100644
--- a/code/global.dm
+++ b/code/global.dm
@@ -9,6 +9,7 @@ var/global/obj/effect/overlay/slmaster = null
var/global/list/machines = list()
var/global/list/processing_objects = list()
var/global/list/active_diseases = list()
+var/global/list/events = list()
//items that ask to be called every cycle
var/global/defer_powernet_rebuild = 0 // true if net rebuild will be called manually after an event
diff --git a/code/modules/events/alien_infestation.dm b/code/modules/events/alien_infestation.dm
new file mode 100644
index 00000000000..1a0cb0d220b
--- /dev/null
+++ b/code/modules/events/alien_infestation.dm
@@ -0,0 +1,32 @@
+/datum/event/alien_infestation
+ announceWhen = 75
+ endWhen = 75
+ oneShot = 1
+
+ var/spawncount = 1
+
+/datum/event/alien_infestation/announce()
+ command_alert("Unidentified lifesigns detected coming aboard [station_name()]. Secure any exterior access, including ducting and ventilation.", "Lifesign Alert")
+ world << sound('sound/AI/aliens.ogg')
+
+
+/datum/event/alien_infestation/start()
+ var/list/vents = list()
+ for(var/obj/machinery/atmospherics/unary/vent_pump/temp_vent in world)
+ if(temp_vent.loc.z == 1 && !temp_vent.welded && temp_vent.network)
+ if(temp_vent.network.normal_members.len > 50) //Stops Aliens getting stuck in small networks. See: Security, Virology
+ vents += temp_vent
+
+ var/list/candidates = get_alien_candidates()
+
+ if(prob(50)) spawncount++ //sometimes, have two larvae spawn instead of one
+ while((spawncount >= 1) && vents.len && candidates.len)
+ var/obj/vent = pick(vents)
+ var/candidate = pick(candidates)
+
+ var/mob/living/carbon/alien/larva/new_xeno = new(vent.loc)
+ new_xeno.key = candidate
+
+ candidates -= candidate
+ vents -= vent
+ spawncount--
\ No newline at end of file
diff --git a/code/modules/events/blob.dm b/code/modules/events/blob.dm
new file mode 100644
index 00000000000..9766757d29a
--- /dev/null
+++ b/code/modules/events/blob.dm
@@ -0,0 +1,25 @@
+/datum/event/blob
+ announceWhen = 12
+ endWhen = 120
+
+ var/obj/effect/blob/core/Blob
+
+
+/datum/event/blob/announce()
+ command_alert("Confirmed outbreak of level 5 biohazard aboard [station_name()]. All personnel must contain the outbreak.", "Biohazard Alert")
+ world << sound('sound/AI/outbreak5.ogg')
+
+
+/datum/event/blob/start()
+ var/turf/T = pick(blobstart)
+ if(!T) kill()
+ Blob = new /obj/effect/blob/core(T, 200)
+ Blob.Life()
+ Blob.Life()
+ Blob.Life()
+
+
+/datum/event/blob/tick()
+ if(!Blob) kill()
+ if(IsMultiple(activeFor, 3))
+ Blob.Life()
\ No newline at end of file
diff --git a/code/modules/events/carp_migration.dm b/code/modules/events/carp_migration.dm
new file mode 100644
index 00000000000..e6c9a34dde2
--- /dev/null
+++ b/code/modules/events/carp_migration.dm
@@ -0,0 +1,14 @@
+/datum/event/carp_migration
+ announceWhen = 50
+ endWhen = 50
+ oneShot = 1
+
+
+/datum/event/carp_migration/announce()
+ command_alert("Unknown biological entities have been detected near [station_name()], please stand-by.", "Lifesign Alert")
+
+
+/datum/event/carp_migration/start()
+ for(var/obj/effect/landmark/C in landmarks_list)
+ if(C.name == "carpspawn")
+ new /mob/living/simple_animal/hostile/carp(C.loc)
\ No newline at end of file
diff --git a/code/modules/events/communications_blackout.dm b/code/modules/events/communications_blackout.dm
new file mode 100644
index 00000000000..18d0c13f979
--- /dev/null
+++ b/code/modules/events/communications_blackout.dm
@@ -0,0 +1,17 @@
+/datum/event/communications_blackout
+ var/silent = 1 //most of the time, we don't want an announcement, so as to allow AIs to fake blackouts.
+
+/datum/event/communications_blackout/announce()
+ if(!silent)
+ command_alert("Ionospheric anomalies detected. Temporary telecommunication failure imminent. Please contact you-BZZT")
+
+
+/datum/event/communications_blackout/start()
+ if(prob(25))
+ silent = 0
+ for(var/mob/living/silicon/ai/A in player_list) //AIs are always aware of communication blackouts.
+ A << "
"
+ A << "Ionospheric anomalies detected. Temporary telecommunication failure imminent. Please contact you-BZZT"
+ A << "
"
+ for(var/obj/machinery/telecomms/T in telecomms_list)
+ T.emp_act(1)
\ No newline at end of file
diff --git a/code/modules/events/disease_outbreak.dm b/code/modules/events/disease_outbreak.dm
new file mode 100644
index 00000000000..9e035a8e274
--- /dev/null
+++ b/code/modules/events/disease_outbreak.dm
@@ -0,0 +1,44 @@
+/datum/event/disease_outbreak
+ announceWhen = 15
+ endWhen = 15
+ oneShot = 1
+
+
+/datum/event/disease_outbreak/announce()
+ command_alert("Confirmed outbreak of level 7 viral biohazard aboard [station_name()]. All personnel must contain the outbreak.", "Biohazard Alert")
+ world << sound('sound/AI/outbreak7.ogg')
+
+/datum/event/disease_outbreak/start()
+ var/virus_type = pick(/datum/disease/dnaspread, /datum/disease/advance/flu, /datum/disease/advance/cold, /datum/disease/brainrot, /datum/disease/magnitis)
+
+ for(var/mob/living/carbon/human/H in shuffle(living_mob_list))
+ var/foundAlready = 0 // don't infect someone that already has the virus
+ var/turf/T = get_turf(H)
+ if(!T)
+ continue
+ if(T.z != 1)
+ continue
+ for(var/datum/disease/D in H.viruses)
+ foundAlready = 1
+ if(H.stat == 2 || foundAlready)
+ continue
+
+ if(virus_type == /datum/disease/dnaspread) //Dnaspread needs strain_data set to work.
+ if((!H.dna) || (H.sdisabilities & BLIND)) //A blindness disease would be the worst.
+ continue
+ var/datum/disease/dnaspread/D = new
+ D.strain_data["name"] = H.real_name
+ D.strain_data["UI"] = H.dna.uni_identity
+ D.strain_data["SE"] = H.dna.struc_enzymes
+ D.carrier = 1
+ D.holder = H
+ D.affected_mob = H
+ H.viruses += D
+ break
+ else
+ var/datum/disease/D = new virus_type
+ D.carrier = 1
+ D.holder = H
+ D.affected_mob = H
+ H.viruses += D
+ break
\ No newline at end of file
diff --git a/code/modules/events/electrical_storm.dm b/code/modules/events/electrical_storm.dm
new file mode 100644
index 00000000000..dacaa09c47a
--- /dev/null
+++ b/code/modules/events/electrical_storm.dm
@@ -0,0 +1,28 @@
+/datum/event/electrical_storm
+ var/lightsoutAmount = 1
+ var/lightsoutRange = 25
+
+
+/datum/event/electrical_storm/announce()
+ command_alert("An electrical storm has been detected in your area, please repair potential electronic overloads.", "Electrical Storm Alert")
+
+
+/datum/event/electrical_storm/start()
+ var/list/epicentreList = list()
+
+ for(var/i=1, i <= lightsoutAmount, i++)
+ var/list/possibleEpicentres = list()
+ for(var/obj/effect/landmark/newEpicentre in landmarks_list)
+ if(newEpicentre.name == "lightsout" && !(newEpicentre in epicentreList))
+ possibleEpicentres += newEpicentre
+ if(possibleEpicentres.len)
+ epicentreList += pick(possibleEpicentres)
+ else
+ break
+
+ if(!epicentreList.len)
+ return
+
+ for(var/obj/effect/landmark/epicentre in epicentreList)
+ for(var/obj/machinery/power/apc/apc in range(epicentre,lightsoutRange))
+ apc.overload_lighting()
\ No newline at end of file
diff --git a/code/modules/events/event.dm b/code/modules/events/event.dm
new file mode 100644
index 00000000000..b6c64b89aac
--- /dev/null
+++ b/code/modules/events/event.dm
@@ -0,0 +1,82 @@
+/datum/event //NOTE: Times are measured in master controller ticks!
+ var/startWhen = 0 //When in the lifetime to call start().
+ var/announceWhen = 0 //When in the lifetime to call announce().
+ var/endWhen = 0 //When in the lifetime the event should end. Include time for delayed announcements.
+ var/oneShot = 0 //If true, then the event removes itself from the list of potential events on creation.
+
+ var/activeFor = 0 //How long the event has existed. You don't need to change this.
+
+
+
+
+//Called when the tick is equal to the startWhen variable.
+//Allows you to announce before starting or vice versa.
+//Only called once.
+/datum/event/proc/start()
+ return
+
+//Called when the tick is equal to the announceWhen variable.
+//Allows you to start before announcing or vice versa.
+//Only called once.
+/datum/event/proc/announce()
+ return
+
+//Called on or after the tick counter is equal to startWhen.
+//You can include code related to your event or add your own
+//time stamped events.
+//Called more than once.
+/datum/event/proc/tick()
+ return
+
+//Called on or after the tick is equal or more than endWhen
+//You can include code related to the event ending.
+//Do not place spawn() in here, instead use tick() to check for
+//the activeFor variable.
+//For example: if(activeFor == myOwnVariable + 30) doStuff()
+//Only called once.
+/datum/event/proc/end()
+ return
+
+
+
+
+//Do not override this proc, instead use the appropiate procs.
+//This proc will handle the calls to the appropiate procs.
+/datum/event/proc/process()
+ if(activeFor == startWhen)
+ start()
+
+ if(activeFor > startWhen)
+ tick()
+
+ if(activeFor == announceWhen)
+ announce()
+
+ if(activeFor >= endWhen)
+ end()
+ kill()
+
+ activeFor++
+
+
+//Garbage collects the event by removing it from the global events list,
+//which should be the only place it's referenced.
+/datum/event/proc/kill()
+ events.Remove(src)
+
+
+//Adds the event to the global events list, and removes it from the list
+//of potential events.
+/datum/event/New()
+ events.Add(src)
+
+ if(oneShot)
+ potentialRandomEvents.Remove(type)
+
+
+//This shouldn't be called, but it ensures that the event doesn't persist in
+//the events list if it'd deleted instead of garbage collected with kill().
+//The master controller will also remove it from the events list if it
+//doesn't exist, so there's redundancy here.
+/datum/event/Del()
+ events.Remove(src)
\ No newline at end of file
diff --git a/code/modules/events/event_manager.dm b/code/modules/events/event_manager.dm
new file mode 100644
index 00000000000..78e89cac7e8
--- /dev/null
+++ b/code/modules/events/event_manager.dm
@@ -0,0 +1,39 @@
+var/list/allEvents = typesof(/datum/event) - /datum/event
+var/list/potentialRandomEvents = typesof(/datum/event) - /datum/event
+
+var/eventTimeLower = 5000
+var/eventTimeUpper = 10000
+
+var/scheduledEvent = null
+
+
+//Currently unused. Needs an admin panel for messing with events.
+/proc/addPotentialEvent(var/type)
+ potentialRandomEvents |= type
+
+/proc/removePotentialEvent(var/type)
+ potentialRandomEvents -= type
+
+
+/proc/checkEvent()
+ if(!scheduledEvent)
+ scheduledEvent = world.timeofday + rand(eventTimeLower, eventTimeUpper)
+
+ else if(world.timeofday > scheduledEvent)
+ spawnEvent()
+
+ scheduledEvent = null
+ checkEvent()
+
+
+/proc/spawnEvent()
+ if(!config.allow_random_events)
+ return
+
+ var/Type = pick(potentialRandomEvents)
+ if(!Type)
+ return
+
+ //The event will add itself to the MC's event list
+ //and start working via the constructor.
+ new Type
\ No newline at end of file
diff --git a/code/modules/events/meteor_wave.dm b/code/modules/events/meteor_wave.dm
new file mode 100644
index 00000000000..fb52bc0582d
--- /dev/null
+++ b/code/modules/events/meteor_wave.dm
@@ -0,0 +1,12 @@
+/datum/event/meteor_wave
+ startWhen = 6
+ endWhen = 66
+
+/datum/event/meteor_wave/announce()
+ command_alert("Meteors have been detected on collision course with the station.", "Meteor Alert")
+ world << sound('sound/AI/meteors.ogg')
+
+
+/datum/event/meteor_wave/tick()
+ if(IsMultiple(activeFor, 3))
+ spawn_meteors(5)
\ No newline at end of file
diff --git a/code/modules/events/prison_break.dm b/code/modules/events/prison_break.dm
new file mode 100644
index 00000000000..783dc6c8fb2
--- /dev/null
+++ b/code/modules/events/prison_break.dm
@@ -0,0 +1,46 @@
+/datum/event/prison_break
+ announceWhen = 50
+ endWhen = 50
+ oneShot = 1
+
+ var/releaseWhen = 25
+ var/list/area/prisonAreas = list()
+
+
+/datum/event/prison_break/announce()
+ if(prisonAreas && prisonAreas.len > 0)
+ command_alert("Gr3y.T1d3 virus detected in [station_name()] imprisonment subroutines. Recommend station AI involvement.", "Security Alert")
+ else
+ world.log << "ERROR: Could not initate grey-tide. Unable find prison or brig area."
+
+
+/datum/event/prison_break/start()
+ for(var/area/A in world)
+ if(istype(A, /area/security/prison) || istype(A, /area/security/brig))
+ prisonAreas += A
+
+ if(prisonAreas && prisonAreas.len > 0)
+ for(var/area/A in prisonAreas)
+ for(var/obj/machinery/light/L in A)
+ L.flicker(10)
+
+
+/datum/event/prison_break/tick()
+ if(activeFor == releaseWhen)
+ if(prisonAreas && prisonAreas.len > 0)
+ for(var/area/A in prisonAreas)
+ for(var/obj/machinery/power/apc/temp_apc in A)
+ temp_apc.overload_lighting()
+
+ for(var/obj/structure/closet/secure_closet/brig/temp_closet in A)
+ temp_closet.locked = 0
+ temp_closet.icon_state = temp_closet.icon_closed
+
+ for(var/obj/machinery/door/airlock/security/temp_airlock in A)
+ temp_airlock.prison_open()
+
+ for(var/obj/machinery/door/airlock/glass_security/temp_glassairlock in A)
+ temp_glassairlock.prison_open()
+
+ for(var/obj/machinery/door_timer/temp_timer in A)
+ temp_timer.releasetime = 1
\ No newline at end of file
diff --git a/code/modules/events/radiation_storm.dm b/code/modules/events/radiation_storm.dm
new file mode 100644
index 00000000000..d709438428d
--- /dev/null
+++ b/code/modules/events/radiation_storm.dm
@@ -0,0 +1,37 @@
+/datum/event/radiation_storm
+ announceWhen = 5
+ endWhen = 5
+ oneShot = 1
+
+
+/datum/event/radiation_storm/announce()
+ command_alert("High levels of radiation detected near the station. Please report to the Med-bay if you feel strange.", "Anomaly Alert")
+ world << sound('sound/AI/radiation.ogg')
+
+
+/datum/event/radiation_storm/start()
+ for(var/mob/living/carbon/human/H in living_mob_list)
+ var/turf/T = get_turf(H)
+ if(!T)
+ continue
+ if(T.z != 1)
+ continue
+ if(istype(H,/mob/living/carbon/human))
+ H.apply_effect((rand(15,75)),IRRADIATE,0)
+ if(prob(5))
+ H.apply_effect((rand(90,150)),IRRADIATE,0)
+ if(prob(25))
+ if (prob(75))
+ randmutb(H)
+ domutcheck(H,null,1)
+ else
+ randmutg(H)
+ domutcheck(H,null,1)
+
+ for(var/mob/living/carbon/monkey/M in living_mob_list)
+ var/turf/T = get_turf(M)
+ if(!T)
+ continue
+ if(T.z != 1)
+ continue
+ M.apply_effect((rand(15,75)),IRRADIATE,0)
\ No newline at end of file
diff --git a/code/modules/events/spacevine.dm b/code/modules/events/spacevine.dm
new file mode 100644
index 00000000000..ee47cc0abbc
--- /dev/null
+++ b/code/modules/events/spacevine.dm
@@ -0,0 +1,5 @@
+/datum/event/spacevine
+ oneShot = 1
+
+/datum/event/spacevine/start()
+ spacevine_infestation()
\ No newline at end of file
diff --git a/code/modules/events/spontaneous_appendicitis.dm b/code/modules/events/spontaneous_appendicitis.dm
new file mode 100644
index 00000000000..470439a29c8
--- /dev/null
+++ b/code/modules/events/spontaneous_appendicitis.dm
@@ -0,0 +1,13 @@
+/datum/event/spontaneous_appendicitis/start()
+ for(var/mob/living/carbon/human/H in living_mob_list)
+ var/foundAlready = 0 //don't infect someone that already has the virus
+ for(var/datum/disease/D in H.viruses)
+ foundAlready = 1
+ if(H.stat == 2 || foundAlready)
+ continue
+
+ var/datum/disease/D = new /datum/disease/appendicitis
+ D.holder = H
+ D.affected_mob = H
+ H.viruses += D
+ break
\ No newline at end of file
diff --git a/tgstation.dme b/tgstation.dme
index 0589a10585c..ffc9133a64a 100644
--- a/tgstation.dme
+++ b/tgstation.dme
@@ -719,6 +719,19 @@
#include "code\modules\detectivework\evidence.dm"
#include "code\modules\detectivework\footprints_and_rag.dm"
#include "code\modules\detectivework\scanner.dm"
+#include "code\modules\events\alien_infestation.dm"
+#include "code\modules\events\blob.dm"
+#include "code\modules\events\carp_migration.dm"
+#include "code\modules\events\communications_blackout.dm"
+#include "code\modules\events\disease_outbreak.dm"
+#include "code\modules\events\electrical_storm.dm"
+#include "code\modules\events\event.dm"
+#include "code\modules\events\event_manager.dm"
+#include "code\modules\events\meteor_wave.dm"
+#include "code\modules\events\prison_break.dm"
+#include "code\modules\events\radiation_storm.dm"
+#include "code\modules\events\spacevine.dm"
+#include "code\modules\events\spontaneous_appendicitis.dm"
#include "code\modules\flufftext\Dreaming.dm"
#include "code\modules\flufftext\Hallucination.dm"
#include "code\modules\flufftext\TextFilters.dm"