diff --git a/code/__defines/subsystems.dm b/code/__defines/subsystems.dm
index 1790890dfbb..39fbe15e2f3 100644
--- a/code/__defines/subsystems.dm
+++ b/code/__defines/subsystems.dm
@@ -73,6 +73,7 @@ var/global/list/runlevel_flags = list(RUNLEVEL_LOBBY, RUNLEVEL_SETUP, RUNLEVEL_G
#define INIT_ORDER_CIRCUIT -21
#define INIT_ORDER_AI -22
#define INIT_ORDER_JOB -23
+#define INIT_ORDER_GAME_MASTER -24
#define INIT_ORDER_CHAT -100 //Should be last to ensure chat remains smooth during init.
diff --git a/code/_helpers/events.dm b/code/_helpers/events.dm
index e31d24783e8..2d15bb3aa9d 100644
--- a/code/_helpers/events.dm
+++ b/code/_helpers/events.dm
@@ -24,4 +24,16 @@
if(A == myarea) //The loc of a turf is the area it is in.
return 1
return 0
-
\ No newline at end of file
+
+// Returns a list of area instances, or a subtypes of them, that are mapped in somewhere.
+// Avoid feeding it `/area`, as it will likely cause a lot of lag as it evaluates every single area coded in.
+/proc/get_all_existing_areas_of_types(list/area_types)
+ . = list()
+ for(var/area_type in area_types)
+ var/list/types = typesof(area_type)
+ for(var/T in types)
+ // Test for existance.
+ var/area/A = locate(T)
+ if(!istype(A) || !A.contents.len) // Empty contents list means it's not on the map.
+ continue
+ . += A
\ No newline at end of file
diff --git a/code/controllers/Processes/game_master.dm b/code/controllers/Processes/game_master.dm
deleted file mode 100644
index 7f89f3ab135..00000000000
--- a/code/controllers/Processes/game_master.dm
+++ /dev/null
@@ -1,6 +0,0 @@
-/datum/controller/process/game_master/setup()
- name = "\improper GM controller"
- schedule_interval = 600 // every 60 seconds
-
-/datum/controller/process/game_master/doWork()
- game_master.process()
\ No newline at end of file
diff --git a/code/controllers/subsystems/events.dm b/code/controllers/subsystems/events.dm
index 6de40f464d5..226c09f4924 100644
--- a/code/controllers/subsystems/events.dm
+++ b/code/controllers/subsystems/events.dm
@@ -1,5 +1,5 @@
SUBSYSTEM_DEF(events)
- name = "Events"
+ name = "Events" // VOREStation Edit - This is still the main events subsystem for us.
wait = 2 SECONDS
var/tmp/list/currentrun = null
diff --git a/code/controllers/subsystems/events2.dm b/code/controllers/subsystems/events2.dm
new file mode 100644
index 00000000000..2a73498b1d6
--- /dev/null
+++ b/code/controllers/subsystems/events2.dm
@@ -0,0 +1,37 @@
+// This is a simple ticker for the new event system.
+// The logic that determines what events get chosen is held inside a seperate subsystem.
+
+SUBSYSTEM_DEF(event_ticker)
+ name = "Events (Ticker)"
+ wait = 2 SECONDS
+ runlevels = RUNLEVEL_GAME
+
+ // List of `/datum/event2/event`s that are currently active, and receiving process() ticks.
+ var/list/active_events = list()
+
+ // List of `/datum/event2/event`s that finished, and are here for showing at roundend, if that's desired.
+ var/list/finished_events = list()
+
+// Process active events.
+/datum/controller/subsystem/event_ticker/fire(resumed)
+ for(var/E in active_events)
+ var/datum/event2/event/event = E
+ event.process()
+ if(event.finished)
+ event_finished(event)
+
+// Starts an event, independent of the GM system.
+// This means it will always run, and won't affect the GM system in any way, e.g. not putting the event off limits after one use.
+/datum/controller/subsystem/event_ticker/proc/start_event(event_type)
+ var/datum/event2/event/E = new event_type()
+ E.execute()
+ event_started(E)
+
+/datum/controller/subsystem/event_ticker/proc/event_started(datum/event2/event/E)
+ log_debug("Event [E.type] is now being ran.")
+ active_events += E
+
+/datum/controller/subsystem/event_ticker/proc/event_finished(datum/event2/event/E)
+ log_debug("Event [E.type] has finished.")
+ active_events -= E
+ finished_events += E
\ No newline at end of file
diff --git a/code/controllers/subsystems/game_master.dm b/code/controllers/subsystems/game_master.dm
new file mode 100644
index 00000000000..d5326bfba61
--- /dev/null
+++ b/code/controllers/subsystems/game_master.dm
@@ -0,0 +1,369 @@
+// This is a sort of successor to the various event systems created over the years. It is designed to be just a tad smarter than the
+// previous ones, checking various things like player count, department size and composition, individual player activity,
+// individual player (IC) skill, and such, in order to try to choose the best events to take in order to add spice or variety to
+// the round.
+
+// This subsystem holds the logic that chooses events. Actual event processing is handled in a seperate subsystem.
+SUBSYSTEM_DEF(game_master)
+ name = "Events (Game Master)"
+ wait = 1 MINUTE
+ runlevels = RUNLEVEL_GAME
+
+ // The GM object is what actually chooses events.
+ // It's held in a seperate object for better encapsulation, and allows for different 'flavors' of GMs to be made, that choose events differently.
+ var/datum/game_master/GM = null
+ var/game_master_type = /datum/game_master/default
+
+ var/list/available_events = list() // A list of meta event objects.
+
+ var/danger = 0 // The GM's best guess at how chaotic the round is. High danger makes it hold back.
+ var/staleness = -20 // Determines liklihood of the GM doing something, increases over time.
+
+ var/next_event = 0 // Minimum amount of time of nothingness until the GM can pick something again.
+
+ var/debug_messages = FALSE // If true, debug information is written to `log_debug()`.
+
+/datum/controller/subsystem/game_master/Initialize()
+ var/list/subtypes = subtypesof(/datum/event2/meta)
+ for(var/T in subtypes)
+ var/datum/event2/meta/M = new T()
+ if(!M.name)
+ continue
+ available_events += M
+
+ GM = new game_master_type()
+
+ if(config && !config.enable_game_master)
+ can_fire = FALSE
+
+ return ..()
+
+/datum/controller/subsystem/game_master/fire(resumed)
+ adjust_staleness(1)
+ adjust_danger(-1)
+
+ var/global_afk = metric.assess_all_living_mobs()
+ global_afk = abs(global_afk - 100)
+ global_afk = round(global_afk / 100, 0.1)
+ adjust_staleness(global_afk) // Staleness increases faster if more people are less active.
+
+ if(GM.ignore_time_restrictions || next_event < world.time)
+ if(prob(staleness) && pre_event_checks())
+ do_event_decision()
+
+
+/datum/controller/subsystem/game_master/proc/do_event_decision()
+ log_game_master("Going to choose an event.")
+ var/datum/event2/meta/event_picked = GM.choose_event()
+ if(event_picked)
+ run_event(event_picked)
+ next_event = world.time + rand(GM.decision_cooldown_lower_bound, GM.decision_cooldown_upper_bound)
+
+/datum/controller/subsystem/game_master/proc/debug_gm()
+ can_fire = TRUE
+ staleness = 100
+ debug_messages = TRUE
+
+/datum/controller/subsystem/game_master/proc/run_event(datum/event2/meta/chosen_event)
+ var/datum/event2/event/E = chosen_event.make_event()
+
+ chosen_event.times_ran++
+
+ if(!chosen_event.reusable)
+ // Disable this event, so it only gets picked once.
+ chosen_event.enabled = FALSE
+ if(chosen_event.event_class)
+ // Disable similar events, too.
+ for(var/M in available_events)
+ var/datum/event2/meta/meta = M
+ if(meta.event_class == chosen_event.event_class)
+ meta.enabled = FALSE
+
+ SSevent_ticker.event_started(E)
+ adjust_danger(chosen_event.chaos)
+ adjust_staleness(-(10 + chosen_event.chaos)) // More chaotic events reduce staleness more, e.g. a 25 chaos event will reduce it by 35.
+
+
+// Tell the game master that something dangerous happened, e.g. someone dying, station explosions.
+/datum/controller/subsystem/game_master/proc/adjust_danger(amount)
+ amount *= GM.danger_modifier
+ danger = round(between(0, danger + amount, 1000), 0.1)
+
+// Tell the game master that things are getting boring if positive, or something interesting if negative..
+/datum/controller/subsystem/game_master/proc/adjust_staleness(amount)
+ amount *= GM.staleness_modifier
+ staleness = round( between(-20, staleness + amount, 100), 0.1)
+
+// These are ran before committing to an event.
+// Returns TRUE if the system is allowed to procede, otherwise returns FALSE.
+/datum/controller/subsystem/game_master/proc/pre_event_checks(quiet = FALSE)
+ if(!ticker || ticker.current_state != GAME_STATE_PLAYING)
+ if(!quiet)
+ log_game_master("Unable to start event: Ticker is nonexistant, or the game is not ongoing.")
+ return FALSE
+ if(GM.ignore_time_restrictions)
+ return TRUE
+ if(next_event > world.time) // Sanity.
+ if(!quiet)
+ log_game_master("Unable to start event: Time until next event is approximately [round((next_event - world.time) / (1 MINUTE))] minute(s)")
+ return FALSE
+
+ // Last minute antagging is bad for humans to do, so the GM will respect the start and end of the round.
+ var/mills = round_duration_in_ticks
+ var/mins = round((mills % 36000) / 600)
+ var/hours = round(mills / 36000)
+
+// if(hours < 1 && mins <= 20) // Don't do anything for the first twenty minutes of the round.
+// if(!quiet)
+// log_debug("Game Master unable to start event: It is too early.")
+// return FALSE
+ if(hours >= 2 && mins >= 40) // Don't do anything in the last twenty minutes of the round, as well.
+ if(!quiet)
+ log_game_master("Unable to start event: It is too late.")
+ return FALSE
+ return TRUE
+
+/datum/controller/subsystem/game_master/proc/choose_game_master(mob/user)
+ var/list/subtypes = subtypesof(/datum/game_master)
+ var/new_gm_path = input(user, "What kind of Game Master do you want?", "New Game Master", /datum/game_master/default) as null|anything in subtypes
+ if(new_gm_path)
+ log_and_message_admins("has swapped the current GM ([GM.type]) for a new GM ([new_gm_path]).")
+ GM = new new_gm_path(src)
+
+/datum/controller/subsystem/game_master/proc/log_game_master(message)
+ if(debug_messages)
+ log_debug("GAME MASTER: [message]")
+
+
+// This object makes the actual decisions.
+/datum/game_master
+ // Multiplier for how much 'danger' is accumulated. Higer generally makes it possible for more dangerous events to be picked.
+ var/danger_modifier = 1.0
+
+ // Ditto. Higher numbers generally result in more events occuring in a round.
+ var/staleness_modifier = 1.0
+
+ var/decision_cooldown_lower_bound = 5 MINUTES // Lower bound for how long to wait until -the potential- for another event being decided.
+ var/decision_cooldown_upper_bound = 20 MINUTES // Same, but upper bound.
+
+ var/ignore_time_restrictions = FALSE // Useful for debugging without needing to wait 20 minutes each time.
+ var/ignore_round_chaos = FALSE // If true, the system will happily choose back to back intense events like meteors and blobs, Dwarf Fortress style.
+
+/client/proc/show_gm_status()
+ set category = "Debug"
+ set name = "Show GM Status"
+ set desc = "Shows you what the GM is thinking. If only that existed in real life..."
+
+ if(check_rights(R_ADMIN|R_EVENT|R_DEBUG))
+ SSgame_master.interact(usr)
+ else
+ to_chat(usr, span("warning", "You do not have sufficent rights to view the GM panel, sorry."))
+
+/datum/controller/subsystem/game_master/proc/interact(var/client/user)
+ if(!user)
+ return
+
+ // Using lists for string tree conservation.
+ var/list/dat = list("
Automated Game Master Event System")
+
+ // Makes the system turn on or off.
+ dat += href(src, list("toggle" = 1), "\[Toggle GM\]")
+ dat += " | "
+
+ // Makes the system not care about staleness or being near round-end.
+ dat += href(src, list("toggle_time_restrictions" = 1), "\[Toggle Time Restrictions\]")
+ dat += " | "
+
+ // Makes the system not care about how chaotic the round might be.
+ dat += href(src, list("toggle_chaos_throttle" = 1), "\[Toggle Chaos Throttling\]")
+ dat += " | "
+
+ // Makes the system immediately choose an event, while still bound to factors like danger, weights, and department staffing.
+ dat += href(src, list("force_choose_event" = 1), "\[Force Event Decision\]")
+ dat += "
"
+
+ // Swaps out the current GM for a new one with different ideas on what a good event might be.
+ dat += href(src, list("change_gm" = 1), "\[Change GM\]")
+ dat += "
"
+
+ dat += "Current GM Type: [GM.type]
"
+ dat += "State: [can_fire ? "Active": "Inactive"]
"
+ dat += "Status: [pre_event_checks(TRUE) ? "Ready" : "Suppressed"]
"
+
+ dat += "Staleness: [staleness] "
+ dat += href(src, list("set_staleness" = 1), "\[Set\]")
+ dat += "
"
+ dat += "Staleness is an estimate of how boring the round might be, and if an event should be done. It is increased passively over time, \
+ and increases faster if people are AFK. It deceases when events and certain 'interesting' things happen in the round.
"
+
+ dat += "Danger: [danger] "
+ dat += href(src, list("set_danger" = 1), "\[Set\]")
+ dat += "
"
+ dat += "Danger is an estimate of how chaotic the round has been so far. It is decreased passively over time, and is increased by having \
+ certain chaotic events be selected, or chaotic things happen in the round. A sufficently high amount of danger will make the system \
+ avoid using destructive events, to avoid pushing the station over the edge.
"
+
+ dat += "Player Activity:
"
+
+ dat += ""
+ dat += ""
+ dat += "| Category | "
+ dat += "Activity Percentage | "
+ dat += "
"
+
+ dat += ""
+ dat += "| All Living Mobs | "
+ dat += "[metric.assess_all_living_mobs()]% | "
+ dat += "
"
+
+ dat += ""
+ dat += "| All Ghosts | "
+ dat += "[metric.assess_all_dead_mobs()]% | "
+ dat += "
"
+
+ dat += ""
+ dat += "| Departments"
+ dat += " |
"
+
+ for(var/D in metric.departments)
+ dat += ""
+ dat += "| [D] | "
+ dat += "[metric.assess_department(D)]% | "
+ dat += "
"
+
+ dat += ""
+ dat += "| Players"
+ dat += " |
"
+
+ for(var/P in player_list)
+ var/mob/M = P
+ dat += ""
+ dat += "| [M] ([M.ckey]) | "
+ dat += "[metric.assess_player_activity(M)]% | "
+ dat += "
"
+ dat += "
"
+
+ dat += "Events available:
"
+
+ dat += ""
+ dat += ""
+ dat += "| Event Name | "
+ dat += "Involved Departments | "
+ dat += "Chaos | "
+ dat += "Chaotic Threshold | "
+ dat += "Weight | "
+ dat += "Buttons | "
+ dat += "
"
+
+ for(var/E in available_events)
+ var/datum/event2/meta/event = E
+ dat += ""
+ if(!event.enabled)
+ dat += "[event.name] | "
+ else
+ dat += "[event.name] | "
+ dat += "[english_list(event.departments)] | "
+ dat += "[event.chaos] | "
+ dat += "[event.chaotic_threshold] | "
+ dat += "[event.get_weight()] | "
+ dat += "[href(event, list("force" = 1), "\[Force\]")] [href(event, list("toggle" = 1), "\[Toggle\]")] | "
+ dat += "
"
+ dat += "
"
+
+ dat += "Events active:
"
+
+ dat += "Current time: [world.time]"
+ dat += ""
+ dat += ""
+ dat += "| Event Type | "
+ dat += "Time Started | "
+ dat += "Time to Announce | "
+ dat += "Time to End | "
+ dat += "Announced | "
+ dat += "Started | "
+ dat += "Ended | "
+ dat += "Buttons | "
+ dat += "
"
+
+ for(var/E in SSevent_ticker.active_events)
+ var/datum/event2/event/event = E
+ dat += ""
+ dat += "| [event.type] | "
+ dat += "[event.time_started] | "
+ dat += "[event.time_to_announce ? event.time_to_announce : "NULL"] | "
+ dat += "[event.time_to_end ? event.time_to_end : "NULL"] | "
+ dat += "[event.announced ? "Yes" : "No"] | "
+ dat += "[event.started ? "Yes" : "No"] | "
+ dat += "[event.ended ? "Yes" : "No"] | "
+ dat += "[href(event, list("abort" = 1), "\[Abort\]")] | "
+ dat += "
"
+ dat += "
"
+ dat += ""
+
+ dat += "Events completed:
"
+
+ dat += ""
+ dat += ""
+ dat += "| Event Type | "
+ dat += "Start Time | "
+ dat += "Finish Time | "
+ dat += "
"
+
+ for(var/E in SSevent_ticker.finished_events)
+ var/datum/event2/event/event = E
+ dat += ""
+ dat += "| [event.type] | "
+ dat += "[event.time_started] | "
+ dat += "[event.time_finished] | "
+ dat += "
"
+
+ dat += "