diff --git a/code/controllers/Processes/game_master.dm b/code/controllers/Processes/game_master.dm
new file mode 100644
index 0000000000..dd4e0d1c23
--- /dev/null
+++ b/code/controllers/Processes/game_master.dm
@@ -0,0 +1,6 @@
+/datum/controller/process/event/setup()
+ name = "\improper GM controller"
+ schedule_interval = 600 // every 60 seconds
+
+/datum/controller/process/event/doWork()
+ game_master.process()
\ No newline at end of file
diff --git a/code/global.dm b/code/global.dm
index 503c953a9e..cd27fbc8ec 100644
--- a/code/global.dm
+++ b/code/global.dm
@@ -118,6 +118,7 @@ var/join_motd = null
var/datum/nanomanager/nanomanager = new() // NanoManager, the manager for Nano UIs.
var/datum/event_manager/event_manager = new() // Event Manager, the manager for events.
+var/datum/game_master/game_master = new() // Game Master, an AI for choosing events.
var/list/awaydestinations = list() // Away missions. A list of landmarks that the warpgate can take you to.
diff --git a/code/modules/admin/admin_verbs.dm b/code/modules/admin/admin_verbs.dm
index 8e1e81c603..3ccfffdf36 100644
--- a/code/modules/admin/admin_verbs.dm
+++ b/code/modules/admin/admin_verbs.dm
@@ -197,7 +197,8 @@ var/list/admin_verbs_debug = list(
/client/proc/dsay,
/client/proc/toggle_debug_logs,
/client/proc/admin_ghost, //allows us to ghost/reenter body at will,
- /datum/admins/proc/view_runtimes
+ /datum/admins/proc/view_runtimes,
+ /client/proc/show_gm_status
)
var/list/admin_verbs_paranoid_debug = list(
diff --git a/code/modules/gamemaster/actions/action.dm b/code/modules/gamemaster/actions/action.dm
new file mode 100644
index 0000000000..82ebd860b5
--- /dev/null
+++ b/code/modules/gamemaster/actions/action.dm
@@ -0,0 +1,29 @@
+/datum/gm_action
+ var/name = "no name" // Simple name, for organization.
+ var/enabled = TRUE // If not enabled, this action is never taken.
+ var/departments = list() // What kinds of departments are affected by this action. Multiple departments can be listed.
+ var/chaotic = 0 // A number showing how chaotic the action may be. If danger is high, the GM will avoid it.
+ var/reusable = FALSE // If true, the event does not become disabled upon being used. Should be used sparingly.
+ var/observers_used = FALSE // Determines if the GM should check if ghosts are available before using this.
+ var/datum/game_master/gm = null
+
+/datum/gm_action/New(var/datum/game_master/new_gm)
+ ..()
+ gm = new_gm
+
+/datum/gm_action/proc/set_up()
+ return
+
+/datum/gm_action/proc/get_weight()
+ return
+
+/datum/gm_action/proc/start()
+ if(!reusable)
+ enabled = FALSE
+ return
+
+/datum/gm_action/proc/end()
+ return
+
+/datum/gm_action/proc/announce()
+ return
\ No newline at end of file
diff --git a/code/modules/gamemaster/actions/comms_blackout.dm b/code/modules/gamemaster/actions/comms_blackout.dm
new file mode 100644
index 0000000000..77814c3f93
--- /dev/null
+++ b/code/modules/gamemaster/actions/comms_blackout.dm
@@ -0,0 +1,6 @@
+// Comms blackout is, just like grid check, mostly the same as always, yet engineering has an option to get it back sooner.
+
+/datum/gm_action/comms_blackout
+ name = "communications blackout"
+ departments = list(ROLE_ENGINEERING, ROLE_EVERYONE)
+ chaotic = 35
\ No newline at end of file
diff --git a/code/modules/gamemaster/actions/grid_check.dm b/code/modules/gamemaster/actions/grid_check.dm
new file mode 100644
index 0000000000..f46f0b162d
--- /dev/null
+++ b/code/modules/gamemaster/actions/grid_check.dm
@@ -0,0 +1,9 @@
+// New grid check event:
+// Very similar to the old one, power goes out in most of the colony, however the new feature is the ability for engineering to
+// get power back on sooner, if they are able to reach a special machine and initiate a manual reboot. If no one is able to do so,
+// it will reboot itself after a few minutes, just like the old one.
+
+/datum/gm_action/grid_check
+ name = "grid check"
+ departments = list(ROLE_ENGINEERING, ROLE_EVERYONE)
+ chaotic = 20
\ No newline at end of file
diff --git a/code/modules/gamemaster/actions/waste_disposal.dm b/code/modules/gamemaster/actions/waste_disposal.dm
new file mode 100644
index 0000000000..4edfba9a1e
--- /dev/null
+++ b/code/modules/gamemaster/actions/waste_disposal.dm
@@ -0,0 +1,6 @@
+// A shuttle full of junk docks, and cargo is tasked with sifting through it all to find valuables, or just dispose of it.
+
+/datum/gm_action/waste_disposal
+ name = "waste disposal"
+ departments = list(ROLE_CARGO)
+ chaotic = 0
\ No newline at end of file
diff --git a/code/modules/gamemaster/controller.dm b/code/modules/gamemaster/controller.dm
new file mode 100644
index 0000000000..abff64ea11
--- /dev/null
+++ b/code/modules/gamemaster/controller.dm
@@ -0,0 +1,40 @@
+/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..."
+
+ game_master.interact(usr)
+
+/datum/game_master/proc/interact(var/client/user)
+ if(!user)
+ return
+
+ var/HTML = "
Game Master AI"
+
+ HTML += "Staleness: [staleness]
"
+ HTML += "Danger: [danger]
"
+
+ HTML += "Actions available;
"
+ for(var/datum/gm_action/action in available_actions)
+ if(action.enabled == FALSE)
+ continue
+ HTML += "[action.name] ([english_list(action.departments)])
"
+
+ HTML += "
"
+ HTML += "All living mobs activity: [assess_all_living_mobs()]
"
+
+ HTML += "
"
+ HTML += "Departmental activity;
"
+ for(var/department in departments)
+ var/number_of_people = count_people_in_department(department)
+ HTML += " [department] : [assess_department(department)] / [number_of_people * 100]
"
+
+ HTML += "
"
+ HTML += "Activity of players;
"
+ for(var/mob/player in player_list)
+ HTML += " [player] : [assess_player_activity(player)]
"
+
+
+
+ HTML +=""
+ user << browse(HTML, "window=log;size=400x450;border=1;can_resize=1;can_close=1;can_minimize=1")
\ No newline at end of file
diff --git a/code/modules/gamemaster/defines.dm b/code/modules/gamemaster/defines.dm
new file mode 100644
index 0000000000..796eb014f8
--- /dev/null
+++ b/code/modules/gamemaster/defines.dm
@@ -0,0 +1,10 @@
+#define ROLE_COMMAND "command"
+#define ROLE_SECURITY "security"
+#define ROLE_ENGINEERING "engineering"
+#define ROLE_MEDICAL "medical"
+#define ROLE_RESEARCH "research"
+#define ROLE_CARGO "cargo"
+#define ROLE_CIVILIAN "civilian"
+#define ROLE_SYNTHETIC "synthetic"
+#define ROLE_UNKNOWN "unknown"
+#define ROLE_EVERYONE "everyone"
\ No newline at end of file
diff --git a/code/modules/gamemaster/game_master.dm b/code/modules/gamemaster/game_master.dm
new file mode 100644
index 0000000000..e21b9631bf
--- /dev/null
+++ b/code/modules/gamemaster/game_master.dm
@@ -0,0 +1,207 @@
+// 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 actions to take in order to add spice or variety to
+// the round.
+
+/datum/game_master
+ var/suspended = FALSE // If true, it will not do anything.
+ var/list/available_actions = list() // A list of 'actions' that the GM has access to, to spice up a round, such as events.
+ 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/danger_modifier = 1 // Multiplier for how much 'danger' is accumulated.
+ var/staleness_modifier = 1 // Ditto. Higher numbers generally result in more events occuring in a round.
+ var/ticks_completed = 0 // Counts amount of ticks completed. Note that this ticks once a minute.
+ var/next_action = 0 // Minimum amount of time of nothingness until the GM can pick something again.
+ var/departments = list( // List of departments the GM considers for choosing events for.
+ ROLE_COMMAND,
+ ROLE_SECURITY,
+ ROLE_ENGINEERING,
+ ROLE_MEDICAL,
+ ROLE_RESEARCH,
+ ROLE_CARGO,
+ ROLE_CIVILIAN,
+ ROLE_SYNTHETIC
+ )
+
+/datum/game_master/New()
+ ..()
+ available_actions = init_subtypes(/datum/gm_action)
+// var/actions = typesof(/datum/gm_actions)
+// for(var/type in actions)
+// available_actions.Add(new type)
+
+/datum/game_master/proc/process()
+ if(ticker && ticker.current_state == GAME_STATE_PLAYING)
+ adjust_staleness(1)
+ adjust_danger(-1)
+ ticks_completed++
+
+ var/global_afk = assess_all_living_mobs()
+ global_afk -= 100
+ global_afk = abs(global_afk)
+ global_afk = round(global_afk / 100, 0.1)
+ adjust_staleness(global_afk) // Staleness increases faster if more people are less active.
+
+ if(world.time < next_action && prob(staleness * 2) )
+ log_debug("Game Master going to start something.")
+ start_action()
+
+/datum/game_master/proc/assess_all_living_mobs()
+ var/num = 0
+ for(var/mob/living/L in player_list) // Ghosts being AFK isn't that much of a concern.
+ . += assess_player_activity(L)
+ num++
+ if(num)
+ . = round(. / num, 0.1)
+
+// This is run before committing to an action/event.
+/datum/game_master/proc/pre_action_checks()
+ if(!ticker || ticker.current_state != GAME_STATE_PLAYING)
+ log_debug("Game Master unable to start event: Ticker is nonexistant, or the game is not ongoing.")
+ 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.
+ 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.
+ log_debug("Game Master unable to start event: It is too late.")
+ return FALSE
+ return TRUE
+
+/datum/game_master/proc/start_action()
+ if(!pre_action_checks()) // Make sure we're not doing last minute events, or early events.
+ return
+ log_debug("Game Master now starting action decision.")
+ var/list/best_actions = assess_round() // Checks the whole round for active people, and returns a list of the most activie departments.
+ if(best_actions && best_actions.len)
+ var/datum/gm_action/choice = pick(best_actions)
+ if(choice)
+// log_debug("[choice.name] was chosen by the Game Master, and is now being ran.")
+// choice.set_up()
+// choice.start()
+// choice.annnounce()
+ next_action = world.time + rand(15 MINUTES, 30 MINUTES)
+
+/datum/game_master/proc/assess_round()
+ var/list/activity = list()
+ for(var/department in departments)
+ activity[department] = assess_department(department)
+ log_debug("Assessing department [department]. They have activity of [activity[department]].")
+
+ var/list/most_active_departments = list() // List of winners.
+ var/highest_activity = null // Department who is leading in activity, if one exists.
+ var/highest_number = 0 // Activity score needed to beat to be the most active department.
+ for(var/i = 1, i <= 3, i++)
+ log_debug("Doing [i]\th round of counting.")
+ for(var/department in activity)
+ if(activity[department] > highest_number && activity[department] > 0) // More active than the current highest department?
+ highest_activity = department
+ highest_number = activity[department]
+
+ if(highest_activity) // Someone's a winner.
+ most_active_departments.Add(highest_activity) // Add to the list of most active.
+ activity.Remove(highest_activity) // Remove them from the other list so they don't win more than once.
+ log_debug("[highest_activity] has won the [i]\th round of activity counting.")
+ highest_activity = null // Now reset for the next round.
+ highest_number = 0
+ //todo: finish
+ var/list/best_actions = decide_best_action(most_active_departments)
+ return best_actions
+
+ // By now, we should have a list of departments populated. The GM will prefer events tailored to these departments.
+
+
+
+
+/datum/game_master/proc/decide_best_action(var/list/most_active_departments)
+ if(!most_active_departments.len) // Server's empty?
+ log_debug("Game Master failed to find any active departments.")
+ return list()
+
+ var/list/best_actions = list() // List of actions which involve the most active departments.
+ if(most_active_departments.len >= 2)
+ for(var/datum/gm_action/action in available_actions)
+ if(!action.enabled)
+ continue
+ // Try to incorporate an action with the top two departments first.
+ if(most_active_departments[1] in action.departments && most_active_departments[2] in action.departments)
+ best_actions.Add(action)
+ log_debug("[action.name] is being considered because both most active departments are involved.")
+
+ if(best_actions.len) // We found something for those two, let's do it.
+ return best_actions
+
+ // Otherwise we probably couldn't find something for the second highest group, so let's ignore them.
+ for(var/datum/gm_action/action in available_actions)
+ if(!action.enabled)
+ continue
+ if(most_active_departments[1] in action.departments)
+ best_actions.Add(action)
+ log_debug("[action.name] is being considered because the most active department is involved.")
+
+ if(best_actions.len) // Found something for the one guy.
+ return best_actions
+
+ // At this point we should expand our horizons.
+ for(var/datum/gm_action/action in available_actions)
+ if(!action.enabled)
+ continue
+ if(ROLE_EVERYONE in action.departments)
+ best_actions.Add(action)
+ log_debug("[action.name] is being considered because it involves everyone.")
+
+ if(best_actions.len) // Finally, perhaps?
+ return best_actions
+
+ // Just give a random event if for some reason it still can't make up its mind.
+ for(var/datum/gm_action/action in available_actions)
+ if(!action.enabled)
+ continue
+ best_actions.Add(action)
+ log_debug("[action.name] is being considered because everything else failed.")
+
+ if(best_actions.len) // Finally, perhaps?
+ return best_actions
+ else
+ log_debug("Game Master failed to find a suitable event, something very wrong is going on.")
+
+// This checks a whole department's viability to receive an event.
+/datum/game_master/proc/assess_department(var/department)
+ if(!department)
+ return
+ var/departmental_activitiy = 0
+ for(var/mob/M in player_list)
+ if(guess_department(M) != department) // Ignore people outside the department we're assessing.
+ continue
+ departmental_activitiy += assess_player_activity(M)
+ return departmental_activitiy
+
+// This checks an individual player's activity level. People who have been afk for a few minutes aren't punished as much as those
+// who were afk for hours, as they're most likely gone for good.
+/datum/game_master/proc/assess_player_activity(var/mob/M)
+ . = 100
+ if(!M)
+ . = 0
+ return
+
+ if(!M.mind || !M.client) // Logged out. They might come back but we can't do any meaningful assessments for now.
+ . = 0
+ return
+
+ var/afk = M.client.is_afk(1 MINUTE)
+ if(afk) // Deduct points based on length of AFK-ness.
+ switch(afk) // One minute is equal to 600, for reference.
+ if(1 MINUTE to 10 MINUTES) // People gone for this emough of time hopefully will come back soon.
+ . -= round( (afk / 200), 1)
+ // . -= 30
+ if(10 MINUTES to 30 MINUTES)
+ . -= round( (afk / 150), 1)
+ // . -= 70
+ if(30 MINUTES to INFINITY) // They're probably not coming back if it's been 30 minutes.
+ . -= 100
+ . = max(. , 0) // No negative numbers, or else people could drag other, non-afk players down.
+
diff --git a/code/modules/gamemaster/helpers.dm b/code/modules/gamemaster/helpers.dm
new file mode 100644
index 0000000000..a0972207a3
--- /dev/null
+++ b/code/modules/gamemaster/helpers.dm
@@ -0,0 +1,72 @@
+// Tell the game master that something dangerous happened, e.g. someone dying.
+/datum/game_master/proc/adjust_danger(var/amt)
+ amt = amt * danger_modifier
+ danger = round( Clamp(danger + amt, 0, 1000), 0.1)
+
+// Tell the game master that something interesting happened.
+/datum/game_master/proc/adjust_staleness(var/amt)
+ amt = amt * staleness_modifier
+ staleness = round( Clamp(staleness + amt, -50, 200), 0.1)
+
+// This proc tries to find the department of an arbitrary mob.
+/datum/game_master/proc/guess_department(var/mob/M)
+ var/datum/data/record/R = find_general_record("name", M.real_name)
+ . = ROLE_UNKNOWN
+ if(R) // We found someone with a record.
+ var/recorded_rank = R.fields["real_rank"]
+ . = role_name_to_department(recorded_rank)
+ if(. != ROLE_UNKNOWN) // We found the correct department, so we can stop now.
+ return
+
+ // They have a custom title, aren't crew, or someone deleted their record, so we need a fallback method.
+ // Let's check the mind.
+ if(M.mind)
+ . = role_name_to_department(M.mind.assigned_role)
+ if(. != ROLE_UNKNOWN)
+ return
+
+ // At this point, they don't have a mind, or for some reason assigned_role didn't work.
+ if(ishuman(M))
+ var/mob/living/carbon/human/H = M
+ . = role_name_to_department(H.job)
+ if(. != ROLE_UNKNOWN)
+ return
+
+ return ROLE_UNKNOWN // Welp.
+
+
+// Feed this proc the name of a job, and it will try to figure out what department they are apart of.
+/datum/game_master/proc/role_name_to_department(var/role_name)
+ if(role_name in security_positions)
+ return ROLE_SECURITY
+
+ if(role_name in engineering_positions)
+ return ROLE_ENGINEERING
+
+ if(role_name in medical_positions)
+ return ROLE_MEDICAL
+
+ if(role_name in science_positions)
+ return ROLE_RESEARCH
+
+ if(role_name in cargo_positions)
+ return ROLE_CARGO
+
+ if(role_name in civilian_positions)
+ return ROLE_CIVILIAN
+
+ if(role_name in nonhuman_positions)
+ return ROLE_SYNTHETIC
+
+ if(role_name in command_positions) // We do command last, so that only the Captain and command secretaries get caught.
+ return ROLE_COMMAND
+
+ return ROLE_UNKNOWN
+
+/datum/game_master/proc/count_people_in_department(var/department)
+ if(!department)
+ return
+ for(var/mob/M in player_list)
+ if(guess_department(M) != department) // Ignore people outside the department we're counting.
+ continue
+ . += 1
\ No newline at end of file
diff --git a/polaris.dme b/polaris.dme
index 16fd036728..b52b6bc082 100644
--- a/polaris.dme
+++ b/polaris.dme
@@ -140,6 +140,7 @@
#include "code\controllers\Processes\chemistry.dm"
#include "code\controllers\Processes\emergencyShuttle.dm"
#include "code\controllers\Processes\event.dm"
+#include "code\controllers\Processes\game_master.dm"
#include "code\controllers\Processes\garbage.dm"
#include "code\controllers\Processes\inactivity.dm"
#include "code\controllers\Processes\machinery.dm"
@@ -1334,6 +1335,14 @@
#include "code\modules\flufftext\Hallucination.dm"
#include "code\modules\flufftext\TextFilters.dm"
#include "code\modules\food\recipes_microwave.dm"
+#include "code\modules\gamemaster\controller.dm"
+#include "code\modules\gamemaster\defines.dm"
+#include "code\modules\gamemaster\game_master.dm"
+#include "code\modules\gamemaster\helpers.dm"
+#include "code\modules\gamemaster\actions\action.dm"
+#include "code\modules\gamemaster\actions\comms_blackout.dm"
+#include "code\modules\gamemaster\actions\grid_check.dm"
+#include "code\modules\gamemaster\actions\waste_disposal.dm"
#include "code\modules\games\cah.dm"
#include "code\modules\games\cah_black_cards.dm"
#include "code\modules\games\cah_white_cards.dm"