diff --git a/code/modules/overmap/disperser/disperser.dm b/code/modules/overmap/disperser/disperser.dm
new file mode 100644
index 0000000000..b51c0fb709
--- /dev/null
+++ b/code/modules/overmap/disperser/disperser.dm
@@ -0,0 +1,65 @@
+//Most interesting stuff happens in disperser_fire.dm
+//This is just basic construction and deconstruction and the like
+
+/obj/machinery/disperser
+ name = "abstract parent for disperser"
+ desc = "You should never see one of these, bap your mappers."
+ icon = 'icons/obj/disperser.dmi'
+ idle_power_usage = 200
+ density = TRUE
+ anchored = TRUE
+
+/obj/machinery/disperser/Initialize()
+ . = ..()
+ // TODO - Remove this bit once machines are converted to Initialize
+ if(ispath(circuit))
+ circuit = new circuit(src)
+ default_apply_parts()
+
+/obj/machinery/disperser/examine(mob/user)
+ . = ..()
+ if(panel_open)
+ to_chat(user, "The maintenance panel is open.")
+
+/obj/machinery/disperser/attackby(obj/item/I, mob/user)
+ if(I && I.is_wrench())
+ if(panel_open)
+ user.visible_message("\The [user] rotates \the [src] with \the [I].",
+ "You rotate \the [src] with \the [I].")
+ set_dir(turn(dir, 90))
+ playsound(src, 'sound/items/jaws_pry.ogg', 50, 1)
+ else
+ to_chat(user, "The maintenance panel must be screwed open for this!")
+ return
+ if(default_deconstruction_screwdriver(user, I))
+ return
+ if(default_deconstruction_crowbar(user, I))
+ return
+ if(default_part_replacement(user, I))
+ return
+ return ..()
+
+/obj/machinery/disperser/front
+ name = "obstruction removal ballista beam generator"
+ desc = "A complex machine which shoots concentrated material beams.\
+
A sign on it reads: STAY CLEAR! DO NOT BLOCK!"
+ icon_state = "front"
+ circuit = /obj/item/weapon/circuitboard/disperserfront
+
+/obj/machinery/disperser/middle
+ name = "obstruction removal ballista fusor"
+ desc = "A complex machine which transmits immense amount of data \
+ from the material deconstructor to the particle beam generator.\
+
A sign on it reads: EXPLOSIVE! DO NOT OVERHEAT!"
+ icon_state = "middle"
+ circuit = /obj/item/weapon/circuitboard/dispersermiddle
+ // maximum_component_parts = list(/obj/item/weapon/stock_parts = 15)
+
+/obj/machinery/disperser/back
+ name = "obstruction removal ballista material deconstructor"
+ desc = "A prototype machine which can deconstruct materials atom by atom.\
+
A sign on it reads: KEEP AWAY FROM LIVING MATERIAL!"
+ icon_state = "back"
+ circuit = /obj/item/weapon/circuitboard/disperserback
+ density = FALSE
+ layer = UNDER_JUNK_LAYER //So the charges go above us.
diff --git a/code/modules/overmap/disperser/disperser_charge.dm b/code/modules/overmap/disperser/disperser_charge.dm
new file mode 100644
index 0000000000..22026086d5
--- /dev/null
+++ b/code/modules/overmap/disperser/disperser_charge.dm
@@ -0,0 +1,51 @@
+/obj/structure/ship_munition/disperser_charge
+ name = "unknown disperser charge"
+ desc = "A charge to power the obstruction removal ballista with. It looks impossibly round and shiny. This charge does not have a defined purpose."
+ icon = 'icons/obj/munitions.dmi'
+ icon_state = "slug"
+ w_class = ITEMSIZE_HUGE
+ density = TRUE
+ // atom_flags = ATOM_FLAG_NO_TEMP_CHANGE | ATOM_FLAG_CLIMBABLE
+ var/chargetype
+ var/chargedesc
+ var/static/list/move_sounds = list( // some nasty sounds to make when moving the board
+ 'sound/effects/metalscrape1.ogg',
+ 'sound/effects/metalscrape2.ogg',
+ 'sound/effects/metalscrape3.ogg'
+ )
+
+// make a screeching noise to drive people mad
+/obj/structure/ship_munition/disperser_charge/Move(atom/newloc, direct = 0)
+ if((. = ..()) && prob(50))
+ var/turf/T = get_turf(src)
+ if(!isspace(T) && !istype(T, /turf/simulated/floor/carpet))
+ playsound(T, pick(move_sounds), 50, 1)
+
+
+/obj/structure/ship_munition/disperser_charge/fire
+ name = "FR1-ENFER charge"
+ color = "#b95a00"
+ desc = "A charge to power the obstruction removal ballista with. It looks impossibly round and shiny. This charge is designed to release a localised fire on impact."
+ chargetype = OVERMAP_WEAKNESS_FIRE
+ chargedesc = "ENFER"
+
+/obj/structure/ship_munition/disperser_charge/emp
+ name = "EM2-QUASAR charge"
+ color = "#6a97b0"
+ desc = "A charge to power the obstruction removal ballista with. It looks impossibly round and shiny. This charge is designed to release a blast of electromagnetic pulse on impact."
+ chargetype = OVERMAP_WEAKNESS_EMP
+ chargedesc = "QUASAR"
+
+/obj/structure/ship_munition/disperser_charge/mining
+ name = "MN3-BERGBAU charge"
+ color = "#cfcf55"
+ desc = "A charge to power the obstruction removal ballista with. It looks impossibly round and shiny. This charge is designed to mine ores on impact."
+ chargetype = OVERMAP_WEAKNESS_MINING
+ chargedesc = "BERGBAU"
+
+/obj/structure/ship_munition/disperser_charge/explosive
+ name = "XP4-INDARRA charge"
+ color = "#aa5f61"
+ desc = "A charge to power the obstruction removal ballista with. It looks impossibly round and shiny. This charge is designed to explode on impact."
+ chargetype = OVERMAP_WEAKNESS_EXPLOSIVE
+ chargedesc = "INDARRA"
diff --git a/code/modules/overmap/disperser/disperser_circuit.dm b/code/modules/overmap/disperser/disperser_circuit.dm
new file mode 100644
index 0000000000..7ecf4ebf1b
--- /dev/null
+++ b/code/modules/overmap/disperser/disperser_circuit.dm
@@ -0,0 +1,35 @@
+#ifndef T_BOARD
+#error T_BOARD macro is not defined but we need it!
+#endif
+
+/obj/item/weapon/circuitboard/disperser
+ name = T_BOARD("obstruction removal ballista control")
+ build_path = /obj/machinery/computer/ship/disperser
+ origin_tech = list(TECH_ENGINEERING = 2, TECH_COMBAT = 2, TECH_BLUESPACE = 2)
+
+/obj/item/weapon/circuitboard/disperserfront
+ name = T_BOARD("obstruction removal ballista beam generator")
+ build_path = /obj/machinery/disperser/front
+ board_type = new /datum/frame/frame_types/machine
+ origin_tech = list(TECH_ENGINEERING = 2, TECH_COMBAT = 2, TECH_BLUESPACE = 2)
+ req_components = list (
+ /obj/item/weapon/stock_parts/manipulator/pico = 5
+ )
+
+/obj/item/weapon/circuitboard/dispersermiddle
+ name = T_BOARD("obstruction removal ballista fusor")
+ build_path = /obj/machinery/disperser/middle
+ board_type = new /datum/frame/frame_types/machine
+ origin_tech = list(TECH_ENGINEERING = 2, TECH_COMBAT = 2, TECH_BLUESPACE = 2)
+ req_components = list (
+ /obj/item/weapon/stock_parts/subspace/crystal = 10
+ )
+
+/obj/item/weapon/circuitboard/disperserback
+ name = T_BOARD("obstruction removal ballista material deconstructor")
+ build_path = /obj/machinery/disperser/back
+ board_type = new /datum/frame/frame_types/machine
+ origin_tech = list(TECH_ENGINEERING = 2, TECH_COMBAT = 2, TECH_BLUESPACE = 2)
+ req_components = list (
+ /obj/item/weapon/stock_parts/capacitor/super = 5
+ )
diff --git a/code/modules/overmap/disperser/disperser_console.dm b/code/modules/overmap/disperser/disperser_console.dm
new file mode 100644
index 0000000000..0128f347c9
--- /dev/null
+++ b/code/modules/overmap/disperser/disperser_console.dm
@@ -0,0 +1,192 @@
+//Amazing disperser from Bxil(tm). Some icons, sounds, and some code shamelessly stolen from ParadiseSS13.
+
+/obj/machinery/computer/ship/disperser
+ name = "obstruction removal ballista control"
+ icon = 'icons/obj/computer.dmi'
+ icon_state = "computer"
+ circuit = /obj/item/weapon/circuitboard/disperser
+
+ core_skill = /datum/skill/pilot
+ var/skill_offset = SKILL_ADEPT - 1 //After which skill level it starts to matter. -1, because we have to index from zero
+
+ icon_keyboard = "rd_key"
+ icon_screen = "teleport"
+
+ var/obj/machinery/disperser/front/front
+ var/obj/machinery/disperser/middle/middle
+ var/obj/machinery/disperser/back/back
+ var/const/link_range = 16 //How far can the above stuff be maximum before we start complaining
+
+ var/overmapdir = 0
+
+ var/caldigit = 4 //number of digits that needs calibration
+ var/list/calibration //what it is
+ var/list/calexpected //what is should be
+
+ var/range = 1 //range of the explosion
+ var/strength = 1 //strength of the explosion
+ var/next_shot = 0 //round time where the next shot can start from
+ var/const/coolinterval = 2 MINUTES //time to wait between safe shots in deciseconds
+
+/obj/machinery/computer/ship/disperser/Initialize()
+ . = ..()
+ link_parts()
+ reset_calibration()
+
+/obj/machinery/computer/ship/disperser/Destroy()
+ release_links()
+ . = ..()
+
+/obj/machinery/computer/ship/disperser/proc/link_parts()
+ if(is_valid_setup())
+ return TRUE
+
+ for(var/obj/machinery/disperser/front/F in global.machines)
+ if(get_dist(src, F) >= link_range)
+ continue
+ var/backwards = turn(F.dir, 180)
+ var/obj/machinery/disperser/middle/M = locate() in get_step(F, backwards)
+ if(!M || get_dist(src, M) >= link_range)
+ continue
+ var/obj/machinery/disperser/back/B = locate() in get_step(M, backwards)
+ if(!B || get_dist(src, B) >= link_range)
+ continue
+ front = F
+ middle = M
+ back = B
+ if(is_valid_setup())
+ GLOB.destroyed_event.register(F, src, .proc/release_links)
+ GLOB.destroyed_event.register(M, src, .proc/release_links)
+ GLOB.destroyed_event.register(B, src, .proc/release_links)
+ return TRUE
+ return FALSE
+
+obj/machinery/computer/ship/disperser/proc/is_valid_setup()
+ if(front && middle && back)
+ var/everything_in_range = (get_dist(src, front) < link_range) && (get_dist(src, middle) < link_range) && (get_dist(src, back) < link_range)
+ var/everything_in_order = (middle.Adjacent(front) && middle.Adjacent(back)) && (front.dir == middle.dir && middle.dir == back.dir)
+ return everything_in_order && everything_in_range
+ return FALSE
+
+/obj/machinery/computer/ship/disperser/proc/release_links()
+ GLOB.destroyed_event.unregister(front, src, .proc/release_links)
+ GLOB.destroyed_event.unregister(middle, src, .proc/release_links)
+ GLOB.destroyed_event.unregister(back, src, .proc/release_links)
+ front = null
+ middle = null
+ back = null
+
+/obj/machinery/computer/ship/disperser/proc/get_calibration()
+ var/list/calresult[caldigit]
+ for(var/i = 1 to caldigit)
+ if(calibration[i] == calexpected[i])
+ calresult[i] = 2
+ else if(calibration[i] in calexpected)
+ calresult[i] = 1
+ else
+ calresult[i] = 0
+ return calresult
+
+/obj/machinery/computer/ship/disperser/proc/reset_calibration()
+ calexpected = new /list(caldigit)
+ calibration = new /list(caldigit)
+ for(var/i = 1 to caldigit)
+ calexpected[i] = rand(0,9)
+ calibration[i] = 0
+
+/obj/machinery/computer/ship/disperser/proc/cal_accuracy()
+ var/top = 0
+ var/divisor = caldigit * 2 //maximum possible value, aka 100% accuracy
+ for(var/i in get_calibration())
+ top += i
+ return round(top * 100 / divisor)
+
+/obj/machinery/computer/ship/disperser/proc/get_next_shot_seconds()
+ return max(0, (next_shot - world.time) / 10)
+
+/obj/machinery/computer/ship/disperser/proc/cool_failchance()
+ return get_next_shot_seconds() * 1000 / coolinterval
+
+/obj/machinery/computer/ship/disperser/proc/get_charge_type()
+ var/obj/structure/ship_munition/disperser_charge/B = locate() in get_turf(back)
+ if(B)
+ return B.chargetype
+ return OVERMAP_WEAKNESS_NONE
+
+/obj/machinery/computer/ship/disperser/proc/get_charge()
+ var/obj/structure/ship_munition/disperser_charge/B = locate() in get_turf(back)
+ if(B)
+ return B
+
+/obj/machinery/computer/ship/disperser/ui_interact(mob/user, ui_key = "main", datum/nanoui/ui = null, force_open = TRUE)
+ if(!linked)
+ display_reconnect_dialog(user, "disperser synchronization")
+ return
+
+ var/data[0]
+
+ if (!link_parts())
+ data["faillink"] = TRUE
+ else
+ data["calibration"] = calibration
+ data["overmapdir"] = overmapdir
+ data["cal_accuracy"] = cal_accuracy()
+ data["strength"] = strength
+ data["range"] = range
+ data["next_shot"] = round(get_next_shot_seconds())
+ data["nopower"] = !data["faillink"] && (!front.powered() || !middle.powered() || !back.powered())
+ data["skill"] = user.get_skill_value(core_skill) > skill_offset
+
+ var/charge = "UNKNOWN ERROR"
+ if(get_charge_type() == OVERMAP_WEAKNESS_NONE)
+ charge = "ERROR: No valid charge detected."
+ else
+ var/obj/structure/ship_munition/disperser_charge/B = get_charge()
+ charge = B.chargedesc
+ data["chargeload"] = charge
+
+ ui = SSnanoui.try_update_ui(user, src, ui_key, ui, data, force_open)
+ if (!ui)
+ ui = new(user, src, ui_key, "disperser.tmpl", "[linked.name] ORB control", 400, 550)
+ ui.set_initial_data(data)
+ ui.open()
+ ui.set_auto_update(1)
+
+/obj/machinery/computer/ship/disperser/OnTopic(mob/user, list/href_list, state)
+ . = ..()
+ if(.)
+ return
+
+ if(!linked)
+ return TOPIC_HANDLED
+
+ if (href_list["choose"])
+ overmapdir = sanitize_integer(text2num(href_list["choose"]), 0, 9, 0)
+ reset_calibration()
+
+ if(href_list["calibration"])
+ var/input = input("0-9", "disperser calibration", 0) as num|null
+ if(!isnull(input)) //can be zero so we explicitly check for null
+ var/calnum = sanitize_integer(text2num(href_list["calibration"]), 0, caldigit)//sanitiiiiize
+ calibration[calnum + 1] = sanitize_integer(input, 0, 9, 0)//must add 1 because nanoui indexes from 0
+
+ if(href_list["skill_calibration"])
+ for(var/i = 1 to min(caldigit, user.get_skill_value(core_skill) - skill_offset))
+ calibration[i] = calexpected[i]
+
+ if(href_list["strength"])
+ var/input = input("1-5", "disperser strength", 1) as num|null
+ if(input && CanInteract(user, state))
+ strength = sanitize_integer(input, 1, 5, 1)
+ middle.idle_power_usage = strength * range * 100
+
+ if(href_list["range"])
+ var/input = input("1-5", "disperser radius", 1) as num|null
+ if(input && CanInteract(user, state))
+ range = sanitize_integer(input, 1, 5, 1)
+ middle.idle_power_usage = strength * range * 100
+
+ if(href_list["fire"])
+ fire(user)
+
+ return TOPIC_REFRESH
diff --git a/code/modules/overmap/disperser/disperser_fire.dm b/code/modules/overmap/disperser/disperser_fire.dm
new file mode 100644
index 0000000000..421c9b4a20
--- /dev/null
+++ b/code/modules/overmap/disperser/disperser_fire.dm
@@ -0,0 +1,100 @@
+/obj/machinery/computer/ship/disperser/proc/fire(mob/user)
+ log_and_message_admins("attempted to launch a disperser beam.")
+ if(!link_parts())
+ return FALSE //no disperser, no service
+ if(!front.powered() || !middle.powered() || !back.powered())
+ return FALSE //no power, no boom boom
+ var/chargetype = get_charge_type()
+ if(chargetype <= 0)
+ return FALSE //no dear, you cannot fire the captain out of a cannon... unless you put him in a box of course
+
+ var/atom/movable/atomcharge = get_charge()
+
+ var/turf/start = front
+ var/direction = front.dir
+
+ var/distance = 0
+ for(var/turf/T in getline(get_step(front,front.dir), get_target_turf(start, direction)))
+ distance++
+ if(T.density)
+ if(distance < 7)
+ explosion(T,1,2,3)
+ continue
+ else
+ T.ex_act(1)
+ for(var/atom/A in T)
+ if(A.density)
+ if(distance < 7)
+ explosion(A,1,2,3)
+ break
+ else
+ A.ex_act(1)
+
+ var/list/relevant_z = GetConnectedZlevels(start.z)
+ for(var/mob/M in global.player_list)
+ var/turf/T = get_turf(M)
+ if(!T || !(T.z in relevant_z))
+ continue
+ shake_camera(M, 25)
+ if(!isdeaf(M))
+ M << sound('sound/effects/explosionfar.ogg', volume=10)
+
+ if(front) //Meanwhile front might have exploded
+ front.layer = ABOVE_JUNK_LAYER //So the beam goes below us. Looks a lot better
+ playsound(start, 'sound/machines/disperser_fire.ogg', 100, 1)
+ handle_beam(start, direction)
+ handle_overbeam()
+ qdel(atomcharge)
+
+ //Some moron disregarded the cooldown warning. Let's blow in their face.
+ if(prob(cool_failchance()))
+ explosion(middle,rand(1,2),rand(2,3),rand(3,4))
+ next_shot = coolinterval + world.time
+
+ //Success, but we missed.
+ if(prob(100 - cal_accuracy()))
+ return TRUE
+
+ reset_calibration()
+
+ var/list/candidates = list()
+
+ for(var/obj/effect/overmap/event/O in get_step(linked, overmapdir))
+ candidates += O
+
+ //Way to waste a charge
+ if(!length(candidates))
+ return TRUE
+
+ var/obj/effect/overmap/event/finaltarget = pick(candidates)
+ log_and_message_admins("A type [chargetype] disperser beam was launched at [finaltarget].", location=finaltarget)
+
+ fire_at_event(finaltarget, chargetype)
+ return TRUE
+
+/obj/machinery/computer/ship/disperser/proc/fire_at_event(obj/effect/overmap/event/finaltarget, chargetype)
+ if(chargetype & finaltarget.weaknesses)
+ var/turf/T = finaltarget.loc
+ qdel(finaltarget)
+ GLOB.overmap_event_handler.update_hazards(T)
+
+/obj/machinery/computer/ship/disperser/proc/handle_beam(turf/start, direction)
+ set waitfor = FALSE
+ start.Beam(get_target_turf(start, direction), "bsa_beam", time = 50, maxdistance = world.maxx)
+ if(front)
+ front.layer = initial(front.layer)
+
+/obj/machinery/computer/ship/disperser/proc/handle_overbeam()
+ set waitfor = FALSE
+ linked.Beam(get_step(linked, overmapdir), "bsa_beam", time = 150, maxdistance = world.maxx)
+
+/obj/machinery/computer/ship/disperser/proc/get_target_turf(turf/start, direction)
+ switch(direction)
+ if(NORTH)
+ return locate(start.x,world.maxy,start.z)
+ if(SOUTH)
+ return locate(start.x,1,start.z)
+ if(WEST)
+ return locate(1,start.y,start.z)
+ if(EAST)
+ return locate(world.maxx,start.y,start.z)
\ No newline at end of file
diff --git a/icons/obj/disperser.dmi b/icons/obj/disperser.dmi
new file mode 100644
index 0000000000..12f4e81776
Binary files /dev/null and b/icons/obj/disperser.dmi differ
diff --git a/icons/obj/munitions.dmi b/icons/obj/munitions.dmi
new file mode 100644
index 0000000000..4631b35ba4
Binary files /dev/null and b/icons/obj/munitions.dmi differ
diff --git a/nano/templates/disperser.tmpl b/nano/templates/disperser.tmpl
new file mode 100644
index 0000000000..08db5ed2ba
--- /dev/null
+++ b/nano/templates/disperser.tmpl
@@ -0,0 +1,91 @@
+{{if data.faillink}}
+