diff --git a/code/controllers/configuration/entries/general.dm b/code/controllers/configuration/entries/general.dm
index 2d9264adf5..5d18337a9f 100644
--- a/code/controllers/configuration/entries/general.dm
+++ b/code/controllers/configuration/entries/general.dm
@@ -459,3 +459,6 @@
/datum/config_entry/number/max_bunker_days
config_entry_value = 7
min_val = 1
+
+/datum/config_entry/flag/minimaps_enabled
+ config_entry_value = TRUE
diff --git a/code/controllers/subsystem/minimaps.dm b/code/controllers/subsystem/minimaps.dm
new file mode 100644
index 0000000000..75de71ca96
--- /dev/null
+++ b/code/controllers/subsystem/minimaps.dm
@@ -0,0 +1,20 @@
+SUBSYSTEM_DEF(minimaps)
+ name = "Minimaps"
+ flags = SS_NO_FIRE
+ var/list/station_minimaps
+ var/datum/minimap_group/station_minimap
+
+/datum/controller/subsystem/minimaps/Initialize()
+ if(!CONFIG_GET(flag/minimaps_enabled))
+ to_chat(world, "Minimaps disabled! Skipping init.")
+ return ..()
+ build_minimaps()
+ return ..()
+
+/datum/controller/subsystem/minimaps/proc/build_minimaps()
+ station_minimaps = list()
+ for(var/z in SSmapping.levels_by_trait(ZTRAIT_STATION))
+ var/datum/space_level/SL = SSmapping.get_level(z)
+ var/name = (SL.name == initial(SL.name))? "[z] - Station" : "[z] - [SL.name]"
+ station_minimaps += new /datum/minimap(z, name = name)
+ station_minimap = new(station_minimaps, "Station")
diff --git a/code/game/area/areas.dm b/code/game/area/areas.dm
index 00dc0d98d6..ac56d6f17c 100644
--- a/code/game/area/areas.dm
+++ b/code/game/area/areas.dm
@@ -63,6 +63,9 @@
var/xenobiology_compatible = FALSE //Can the Xenobio management console transverse this area by default?
var/list/canSmoothWithAreas //typecache to limit the areas that atoms in this area can smooth with
+ /// Color on minimaps, if it's null (which is default) it makes one at random.
+ var/minimap_color
+
/**
* These two vars allow for multiple unique areas to be linked to a master area
* and share some functionalities such as APC powernet nodes, fire alarms etc, without sacrificing
@@ -96,7 +99,14 @@ GLOBAL_LIST_EMPTY(teleportlocs)
// ===
/area/New()
- // This interacts with the map loader, so it needs to be set immediately
+ if(!minimap_color) // goes in New() because otherwise it doesn't fucking work
+ // generate one using the icon_state
+ if(icon_state && icon_state != "unknown")
+ var/icon/I = new(icon, icon_state, dir)
+ I.Scale(1,1)
+ minimap_color = I.GetPixel(1,1)
+ else // no icon state? use random.
+ minimap_color = rgb(rand(50,70),rand(50,70),rand(50,70)) // This interacts with the map loader, so it needs to be set immediately
// rather than waiting for atoms to initialize.
if (unique)
GLOB.areas_by_type[type] = src
diff --git a/code/modules/client/verbs/minimap.dm b/code/modules/client/verbs/minimap.dm
new file mode 100644
index 0000000000..3d213dc210
--- /dev/null
+++ b/code/modules/client/verbs/minimap.dm
@@ -0,0 +1,10 @@
+/client/verb/show_station_minimap()
+ set category = "OOC"
+ set name = "Show Station Minimap"
+ set desc = "Shows a minimap of the currently loaded station map."
+
+ if(!CONFIG_GET(flag/minimaps_enabled))
+ to_chat(usr, "Minimap generation is not enabled in the server's configuration.")
+ return
+
+ SSminimaps.station_minimap.show(src)
diff --git a/code/modules/mapping/minimaps.dm b/code/modules/mapping/minimaps.dm
new file mode 100644
index 0000000000..c347a8d7a6
--- /dev/null
+++ b/code/modules/mapping/minimaps.dm
@@ -0,0 +1,155 @@
+/datum/minimap
+ var/name
+ var/icon/map_icon
+ var/icon/meta_icon
+ var/icon/overlay_icon
+ var/list/color_area_names = list()
+ var/minx
+ var/maxx
+ var/miny
+ var/maxy
+ var/z_level
+ var/id = 0
+ var/static/next_id = 0
+
+/datum/minimap/New(z, x1 = 1, y1 = 1, x2 = world.maxx, y2 = world.maxy, name)
+ src.name = name
+ id = ++next_id
+ z_level = z
+
+ var/crop_x1 = x2
+ var/crop_x2 = x1
+ var/crop_y1 = y2
+ var/crop_y2 = y1
+
+ // do the generating
+ map_icon = new('html/blank.png')
+ meta_icon = new('html/blank.png')
+ map_icon.Scale(x2-x1+1, y2-y1+1) // arrays start at 1
+ meta_icon.Scale(x2-x1+1, y2-y1+1)
+ var/list/area_to_color = list()
+ for(var/turf/T in block(locate(x1,y1,z),locate(x2,y2,z)))
+ var/area/A = T.loc
+ var/img_x = T.x - x1 + 1 // arrays start at 1
+ var/img_y = T.y - y1 + 1
+ if(!istype(A, /area/space) || istype(T, /turf/closed/wall))
+ crop_x1 = min(crop_x1, T.x)
+ crop_x2 = max(crop_x2, T.x)
+ crop_y1 = min(crop_y1, T.y)
+ crop_y2 = max(crop_y2, T.y)
+ var/meta_color = area_to_color[A]
+ if(!meta_color)
+ meta_color = rgb(rand(0,255),rand(0,255),rand(0,255)) // technically conflicts could happen but it's like very unlikely and it's not that big of a deal if one happens
+ area_to_color[A] = meta_color
+ color_area_names[meta_color] = A.name
+ meta_icon.DrawBox(meta_color, img_x, img_y)
+ if(istype(T, /turf/closed/wall))
+ map_icon.DrawBox("#000000", img_x, img_y)
+ else if(!istype(A, /area/space))
+ var/color = A.minimap_color || "#FF00FF"
+ if(locate(/obj/machinery/power/solar) in T)
+ color = "#02026a"
+ if((locate(/obj/effect/spawner/structure/window) in T) || (locate(/obj/structure/grille) in T))
+ color = BlendRGB(color, "#000000", 0.5)
+ map_icon.DrawBox(color, img_x, img_y)
+ map_icon.Crop(crop_x1, crop_y1, crop_x2, crop_y2)
+ meta_icon.Crop(crop_x1, crop_y1, crop_x2, crop_y2)
+ minx = crop_x1
+ maxx = crop_x2
+ miny = crop_y1
+ maxy = crop_y2
+ overlay_icon = new(map_icon)
+ overlay_icon.Scale(16, 16)
+
+/datum/minimap/proc/send(mob/user)
+ register_asset("minimap-[id].png", map_icon)
+ register_asset("minimap-[id]-meta.png", meta_icon)
+ send_asset_list(user, list("minimap-[id].png" = map_icon, "minimap-[id]-meta.png" = meta_icon), verify=FALSE)
+
+/datum/minimap_group
+ var/list/minimaps
+ var/static/next_id = 0
+ var/id
+ var/name
+
+/datum/minimap_group/New(list/maps, name)
+ id = ++next_id
+ src.name = name
+ minimaps = maps || list()
+
+/datum/minimap_group/proc/show(mob/user)
+ if(!length(minimaps))
+ to_chat(user, "ERROR: Attempted to access an empty datum/minimap_group. This should probably not happen.")
+ return
+ var/list/datas = list()
+ var/list/info = list()
+ var/datum/minimap/first_map = minimaps[1]
+ for(var/i in 1 to length(minimaps))
+ var/datum/minimap/M = minimaps[i]
+ M.send(user)
+ info += "