diff --git a/code/game/machinery/flatpacker.dm b/code/game/machinery/flatpacker.dm
index 6c90e45e4f6..93bb14323aa 100644
--- a/code/game/machinery/flatpacker.dm
+++ b/code/game/machinery/flatpacker.dm
@@ -476,3 +476,9 @@
return ITEM_INTERACT_SUCCESS
#undef MAX_FLAT_PACKS
+
+/obj/item/flatpack/flatpacker // a roundstart flatpacker is NICE you can gahdamn tell the time and everythin'
+ board = /obj/item/circuitboard/machine/flatpacker
+
+/obj/item/flatpack/mailsorter // to have a roundstart mail sorter at cargo
+ board = /obj/item/circuitboard/machine/mailsorter
diff --git a/code/game/objects/items/circuitboards/machines/machine_circuitboards.dm b/code/game/objects/items/circuitboards/machines/machine_circuitboards.dm
index a0685695cfa..e677d160980 100644
--- a/code/game/objects/items/circuitboards/machines/machine_circuitboards.dm
+++ b/code/game/objects/items/circuitboards/machines/machine_circuitboards.dm
@@ -1475,6 +1475,16 @@
/datum/stock_part/scanning_module = 1,
/datum/stock_part/card_reader = 1)
+/obj/item/circuitboard/machine/mailsorter
+ name = "Mail Sorter"
+ greyscale_colors = CIRCUIT_COLOR_SUPPLY
+ build_path = /obj/machinery/mailsorter
+ req_components = list(
+ /obj/item/stack/sheet/glass = 1,
+ /datum/stock_part/matter_bin = 2,
+ /datum/stock_part/scanning_module = 1)
+ needs_anchored = TRUE
+
//Tram
/obj/item/circuitboard/machine/crossing_signal
name = "Crossing Signal"
diff --git a/code/modules/research/designs/machine_designs.dm b/code/modules/research/designs/machine_designs.dm
index 15a5fed08eb..74e00231035 100644
--- a/code/modules/research/designs/machine_designs.dm
+++ b/code/modules/research/designs/machine_designs.dm
@@ -1347,3 +1347,13 @@
RND_CATEGORY_MACHINE + RND_SUBCATEGORY_MACHINE_ENGINEERING
)
departmental_flags = DEPARTMENT_BITFLAG_ENGINEERING | DEPARTMENT_BITFLAG_CARGO
+
+/datum/design/board/mailsorter
+ name = "Mail Sorter"
+ desc = "The circuit board for a mail sorting unit."
+ id = "mailsorter"
+ build_path = /obj/item/circuitboard/machine/mailsorter
+ category = list(
+ RND_CATEGORY_MACHINE + RND_SUBCATEGORY_MACHINE_CARGO
+ )
+ departmental_flags = DEPARTMENT_BITFLAG_CARGO | DEPARTMENT_BITFLAG_ENGINEERING
diff --git a/code/modules/research/techweb/nodes/engi_nodes.dm b/code/modules/research/techweb/nodes/engi_nodes.dm
index 4ef55e21bc9..75c9459771c 100644
--- a/code/modules/research/techweb/nodes/engi_nodes.dm
+++ b/code/modules/research/techweb/nodes/engi_nodes.dm
@@ -155,6 +155,7 @@
"manulathe",
"manusorter",
"manurouter",
+ "mailsorter",
)
/datum/techweb_node/energy_manipulation
diff --git a/code/modules/vending/mail.dm b/code/modules/vending/mail.dm
new file mode 100644
index 00000000000..6bc3648b056
--- /dev/null
+++ b/code/modules/vending/mail.dm
@@ -0,0 +1,314 @@
+#define STATE_SORTING "sorting"
+#define STATE_IDLE "idle"
+#define STATE_YES "yes"
+#define STATE_NO "no"
+#define MAIL_CAPACITY 100
+
+/obj/machinery/mailsorter
+ name = "mail sorter"
+ desc = "A large mail sorting unit. Sorting mail since 1987!"
+ icon = 'icons/obj/machines/mailsorter.dmi'
+ icon_state = "mailsorter"
+ base_icon_state = "mailsorter"
+ layer = BELOW_OBJ_LAYER
+ density = TRUE
+ max_integrity = 300
+ integrity_failure = 0.33
+ req_access = list(ACCESS_CARGO)
+ circuit = /obj/item/circuitboard/machine/mailsorter
+
+ var/light_mask = "mailsorter-light-mask"
+ var/panel_type = "panel"
+
+ /// What the machine is currently doing. Can be "sorting", "idle", "yes", "no".
+ var/currentstate = STATE_IDLE
+ /// List of all mail that's inside the mailbox.
+ var/list/mail_list = list()
+ /// The direction in which the mail will be unloaded.
+ var/output_dir = SOUTH
+ /// List of the departments to sort the mail for.
+ var/static/list/sorting_departments = list(
+ DEPARTMENT_ENGINEERING,
+ DEPARTMENT_SECURITY,
+ DEPARTMENT_MEDICAL,
+ DEPARTMENT_SCIENCE,
+ DEPARTMENT_CARGO,
+ DEPARTMENT_SERVICE,
+ DEPARTMENT_COMMAND,
+ )
+ var/static/list/choices = list(
+ "Eject" = icon('icons/hud/radial.dmi', "radial_eject"),
+ "Dump" = icon('icons/hud/radial.dmi', "mail_dump"),
+ "Sort" = icon('icons/hud/radial.dmi', "mail_sort"),
+ )
+
+/// Steps one tile in the `output_dir`. Returns `turf`.
+/obj/machinery/mailsorter/proc/get_unload_turf()
+ return get_step(src, output_dir)
+
+/// Opening the maintenance panel.
+/obj/machinery/mailsorter/screwdriver_act(mob/living/user, obj/item/tool)
+ default_deconstruction_screwdriver(user, "[base_icon_state]-off", base_icon_state, tool)
+ update_appearance(UPDATE_OVERLAYS)
+ return ITEM_INTERACT_SUCCESS
+
+/// Deconstructing the mail sorter.
+/obj/machinery/mailsorter/crowbar_act(mob/living/user, obj/item/tool)
+ default_deconstruction_crowbar(tool)
+ return ITEM_INTERACT_SUCCESS
+
+/obj/machinery/mailsorter/examine(mob/user)
+ . = ..()
+ . += span_notice("There is[length(mail_list) < 100 ? " " : " no more "]space for [length(mail_list) < 100 ? "[100 - length(mail_list)] " : ""]envelope\s inside.")
+ . += span_notice("There [length(mail_list) >= 2 ? "are" : "is"] [length(mail_list) ? length(mail_list) : "no"] envelope\s inside.")
+ if(panel_open)
+ . += span_notice("Alt-click to rotate the output direction.")
+
+/obj/machinery/mailsorter/Destroy()
+ QDEL_LIST(mail_list)
+ . = ..()
+
+/obj/machinery/mailsorter/on_deconstruction(disassembled)
+ drop_all_mail()
+ . = ..()
+
+/// Drops all enevlopes on the machine turf.
+/obj/machinery/mailsorter/proc/drop_all_mail()
+ if(!isturf(get_turf(src)))
+ QDEL_LIST(mail_list)
+ return
+ for(var/obj/item/mail in mail_list)
+ mail.forceMove(src)
+ mail_list -= mail
+
+/// Dumps all envelopes on the `unload_turf`.
+/obj/machinery/mailsorter/proc/dump_all_mail()
+ if(!isturf(get_turf(src)))
+ QDEL_LIST(mail_list)
+ return
+ var/turf/unload_turf = get_unload_turf()
+ for(var/obj/item/mail in mail_list)
+ mail.forceMove(unload_turf)
+ mail.throw_at(unload_turf, 2, 3)
+ mail_list -= mail
+
+/// Validates whether the inserted item is acceptable.
+/obj/machinery/mailsorter/proc/accept_check(obj/item/weapon)
+ var/static/list/accepted_items = list(
+ /obj/item/mail,
+ /obj/item/paper,
+ )
+ return is_type_in_list(weapon, accepted_items)
+
+/obj/machinery/mailsorter/interact(mob/user)
+ if (!allowed(user))
+ to_chat(user, span_warning("Access denied."))
+ return
+ if (currentstate != STATE_IDLE)
+ return
+ if (length(mail_list) == 0)
+ to_chat(user, span_warning("There's no mail inside!"))
+ return
+ var/choice = show_radial_menu(
+ user,
+ src,
+ choices,
+ require_near = !HAS_SILICON_ACCESS(user),
+ autopick_single_option = FALSE,
+ )
+ if (!choice)
+ return
+ switch (choice)
+ if ("Eject")
+ pick_mail(user)
+ if ("Dump")
+ playsound(src, 'sound/machines/buzz/buzz-sigh.ogg', 20, TRUE)
+ to_chat(user, span_notice("[src] dumps [length(mail_list)] envelope\s on the floor."))
+ dump_all_mail()
+ if ("Sort")
+ sort_mail(user)
+
+/// Prompts the player to select a department to sort the mail for. Returns if `null`.
+/obj/machinery/mailsorter/proc/sort_mail(mob/user)
+ var/sorting_dept = tgui_input_list(user, "Choose the department to sort mail for","Mail Sorting", sorting_departments)
+ if (!sorting_dept)
+ return
+ currentstate = STATE_SORTING
+ update_appearance(UPDATE_OVERLAYS)
+ playsound(src, 'sound/machines/mail_sort.ogg', 20, TRUE)
+ addtimer(CALLBACK(src, PROC_REF(continue_sort), user, sorting_dept), 5 SECONDS)
+
+/// Sorts the mail based on the picked department. Ejects the sorted envelopes onto the `unload_turf`.
+/obj/machinery/mailsorter/proc/continue_sort(mob/user, sorting_dept)
+ var/list/sorted_mail = list()
+ var/total_to_sort = length(mail_list)
+ var/sorted = 0
+ var/unable_to_sort = 0
+
+ for (var/obj/item/mail/some_mail in mail_list)
+ if (!some_mail.recipient_ref)
+ unable_to_sort ++
+ continue
+ var/datum/mind/some_recipient = some_mail.recipient_ref.resolve()
+ if (some_recipient)
+ var/datum/job/recipient_job = some_recipient.assigned_role
+ var/datum/job_department/primary_department = recipient_job.departments_list?[1]
+ if (primary_department == null) // permabrig is temporary, tide is forever
+ unable_to_sort ++
+ else
+ var/datum/job_department/main_department = primary_department.department_name
+ if (main_department == sorting_dept)
+ sorted_mail.Add(some_mail)
+ sorted ++
+ else
+ unable_to_sort ++
+ if (length(sorted_mail) == 0)
+ currentstate = STATE_NO
+ update_appearance(UPDATE_OVERLAYS)
+ playsound(src, 'sound/machines/buzz/buzz-sigh.ogg', 20, TRUE)
+ say("No mail for the following department: [sorting_dept].")
+ else
+ currentstate = STATE_YES
+ update_appearance(UPDATE_OVERLAYS)
+ say("[sorted] envelope\s sorted successfully.")
+ playsound(src, 'sound/machines/ping.ogg', 20, TRUE)
+ to_chat(user, span_notice("[src] ejects [length(sorted_mail)] envelope\s."))
+ var/turf/unload_turf = get_unload_turf()
+ for (var/obj/item/mail/mail_in_list in sorted_mail)
+ mail_in_list.forceMove(unload_turf)
+ sorted_mail -= mail_in_list
+ mail_list -= mail_in_list
+ addtimer(CALLBACK(src, PROC_REF(check_sorted), unable_to_sort, total_to_sort), 1 SECONDS)
+
+/// Informs the player of the amount of processed envelopes.
+/obj/machinery/mailsorter/proc/check_sorted(mob/user, unable_to_sort, total_to_sort)
+ if (unable_to_sort > 0)
+ playsound(src, 'sound/machines/buzz/buzz-sigh.ogg', 20, TRUE)
+ say("Couldn't sort [unable_to_sort] envelope\s.")
+ else
+ playsound(src, 'sound/machines/ping.ogg', 20, TRUE)
+ say("[total_to_sort] envelope\s processed.")
+ addtimer(CALLBACK(src, PROC_REF(update_state_after_sorting)), 1 SECONDS)
+
+/obj/machinery/mailsorter/proc/update_state_after_sorting()
+ currentstate = STATE_IDLE
+ update_appearance(UPDATE_OVERLAYS)
+
+/obj/machinery/mailsorter/item_interaction(mob/user, obj/item/thingy, params)
+ if (istype(thingy, /obj/item/storage/bag/mail))
+ if (length(thingy.contents) < 1)
+ to_chat(user, span_warning("The [thingy] is empty!"))
+ return
+ var/loaded = 0
+ for (var/obj/item/mail in thingy.contents)
+ if (!(mail.item_flags & ABSTRACT) && \
+ !(mail.flags_1 & HOLOGRAM_1) && \
+ accept_check(mail) \
+ )
+ if (length(mail_list) + 1 > MAIL_CAPACITY )
+ to_chat(user, span_warning("There is no space for more mail in [src]!"))
+ return FALSE
+ else if (load(mail, user))
+ loaded++
+ mail_list += mail
+ if(loaded)
+ user.visible_message(span_notice("[user] loads \the [src] with \the [thingy]."), \
+ span_notice("You load \the [src] with \the [thingy]."))
+ if(length(thingy.contents))
+ to_chat(user, span_warning("Some items are refused."))
+ return TRUE
+ else
+ to_chat(user, span_warning("There is nothing in \the [thingy] to put in the [src]!"))
+ return FALSE
+ else if (istype(thingy, /obj/item/mail))
+ if (length(mail_list) + 1 > MAIL_CAPACITY )
+ to_chat(user, span_warning("There is no space for more mail in [src]!"))
+ else
+ thingy.forceMove(src)
+ mail_list += thingy
+ to_chat(user, span_notice("The [src] whizzles as it accepts the [thingy]."))
+
+/// Prompts the user to select an anvelope from the list of all the envelopes inside.
+/obj/machinery/mailsorter/proc/pick_mail(mob/user)
+ if(!length(mail_list))
+ return
+ var/obj/item/mail/mail_throw = tgui_input_list(user, "Choose the envelope to eject","Mail Sorting", mail_list)
+ if(!mail_throw)
+ return
+ currentstate = STATE_SORTING
+ update_appearance(UPDATE_OVERLAYS)
+ playsound(src, 'sound/machines/mail_sort.ogg', 20, TRUE)
+ addtimer(CALLBACK(src, PROC_REF(pick_envelope), user, mail_throw), 50)
+
+/// Ejects a single envelope the player has picked onto the `unload_turf`.
+/obj/machinery/mailsorter/proc/pick_envelope(mob/user, obj/item/mail/mail_throw)
+ to_chat(user, span_notice("[src] reluctantly spits out [mail_throw]."))
+ var/turf/unload_turf = get_unload_turf()
+ mail_throw.forceMove(unload_turf)
+ mail_throw.throw_at(unload_turf, 2, 3)
+ mail_list -= mail_throw
+ currentstate = STATE_IDLE
+ update_appearance(UPDATE_OVERLAYS)
+
+/// Tries to load something into the machine.
+/obj/machinery/mailsorter/proc/load(obj/item/thingy, mob/user)
+ if(ismob(thingy.loc))
+ var/mob/owner = thingy.loc
+ if(!owner.transferItemToLoc(thingy, src))
+ to_chat(owner, span_warning("\the [thingy] is stuck to your hand, you cannot put it in \the [src]!"))
+ return FALSE
+ return TRUE
+ else
+ if(thingy.loc.atom_storage)
+ return thingy.loc.atom_storage.attempt_remove(thingy, src, silent = TRUE)
+ else
+ thingy.forceMove(src)
+ return TRUE
+
+/obj/machinery/mailsorter/click_alt(mob/living/user)
+ if(!panel_open)
+ return CLICK_ACTION_BLOCKING
+ output_dir = turn(output_dir, -90)
+ to_chat(user, span_notice("You change [src]'s I/O settings, setting the output to [dir2text(output_dir)]."))
+ update_appearance(UPDATE_OVERLAYS)
+ return CLICK_ACTION_SUCCESS
+
+
+/obj/machinery/mailsorter/update_overlays()
+ . = ..()
+ if(!powered())
+ return
+ if(!(machine_stat & BROKEN))
+ var/image/mail_output = image(icon='icons/obj/doors/airlocks/station/overlays.dmi', icon_state="unres_[output_dir]")
+ switch(output_dir)
+ if(NORTH)
+ mail_output.pixel_y = 32
+ if(SOUTH)
+ mail_output.pixel_y = -32
+ if(EAST)
+ mail_output.pixel_x = 32
+ if(WEST)
+ mail_output.pixel_x = -32
+ mail_output.color = COLOR_CRAYON_ORANGE
+ var/mutable_appearance/light_out = emissive_appearance(mail_output.icon, mail_output.icon_state, offset_spokesman = src, alpha = mail_output.alpha)
+ light_out.pixel_y = mail_output.pixel_y
+ light_out.pixel_x = mail_output.pixel_x
+ . += mail_output
+ . += light_out
+ . += mutable_appearance(base_icon_state, currentstate)
+ if(panel_open)
+ . += panel_type
+ if(light_mask && !(machine_stat & BROKEN))
+ . += emissive_appearance(icon, light_mask, src)
+
+/obj/machinery/mailsorter/update_icon_state()
+ icon_state = "[base_icon_state][powered() ? null : "-off"]"
+ if(machine_stat & BROKEN)
+ icon_state = "[base_icon_state]-broken"
+ return ..()
+
+#undef STATE_SORTING
+#undef STATE_IDLE
+#undef STATE_YES
+#undef STATE_NO
+#undef MAIL_CAPACITY
diff --git a/code/modules/vending/wardrobes.dm b/code/modules/vending/wardrobes.dm
index 4d37e2eb646..bec7da995df 100644
--- a/code/modules/vending/wardrobes.dm
+++ b/code/modules/vending/wardrobes.dm
@@ -204,6 +204,7 @@ GLOBAL_VAR_INIT(roaches_deployed, FALSE)
/obj/item/storage/bag/mail = 3,
/obj/item/radio/headset/headset_cargo = 3,
/obj/item/clothing/accessory/pocketprotector = 3,
+ /obj/item/flatpack/mailsorter = 1,
)
premium = list(
/obj/item/clothing/head/costume/mailman = 1,
diff --git a/html/changelogs/AutoChangeLog-pr-88288.yml b/html/changelogs/AutoChangeLog-pr-88288.yml
new file mode 100644
index 00000000000..d785607b372
--- /dev/null
+++ b/html/changelogs/AutoChangeLog-pr-88288.yml
@@ -0,0 +1,5 @@
+author: "mcbalaam"
+delete-after: True
+changes:
+ - rscadd: "Added the mail sorting unit - working with mail has never been simpler!"
+ - rscadd: "Added two flatpack pre-defined subtypes for the flatpacker and the mail sorter."
\ No newline at end of file
diff --git a/html/changelogs/AutoChangeLog-pr-88562.yml b/html/changelogs/AutoChangeLog-pr-88562.yml
new file mode 100644
index 00000000000..9405b08a5a2
--- /dev/null
+++ b/html/changelogs/AutoChangeLog-pr-88562.yml
@@ -0,0 +1,4 @@
+author: "mcbalaam"
+delete-after: True
+changes:
+ - bugfix: "The mail sorter no longer runtimes processing assistant mail"
\ No newline at end of file
diff --git a/icons/hud/radial.dmi b/icons/hud/radial.dmi
index f6e141ab685..5e32a89fe5d 100644
Binary files a/icons/hud/radial.dmi and b/icons/hud/radial.dmi differ
diff --git a/icons/obj/machines/mailsorter.dmi b/icons/obj/machines/mailsorter.dmi
new file mode 100644
index 00000000000..8d09e36796f
Binary files /dev/null and b/icons/obj/machines/mailsorter.dmi differ
diff --git a/icons/obj/machines/vending.dmi b/icons/obj/machines/vending.dmi
index 319771e4e7f..8c39296a155 100644
Binary files a/icons/obj/machines/vending.dmi and b/icons/obj/machines/vending.dmi differ
diff --git a/sound/machines/license.txt b/sound/machines/license.txt
index dbccfd7ea09..69e52c94e4b 100644
--- a/sound/machines/license.txt
+++ b/sound/machines/license.txt
@@ -4,3 +4,5 @@ This is licensed under CC-BY 4.0, found at https://creativecommons.org/licenses/
shutter.ogg adapted from Joseph Sardin on BigSoundBank
https://bigsoundbank.com/detail-2475-manual-roller-shutter-closing-out-2.html
+
+mail_sort.ogg adapted from csigusz_foxoup ob Freesound https://freesound.org/people/csigusz_foxoup/sounds/711428/
\ No newline at end of file
diff --git a/sound/machines/mail_sort.ogg b/sound/machines/mail_sort.ogg
new file mode 100644
index 00000000000..66ec79468d5
Binary files /dev/null and b/sound/machines/mail_sort.ogg differ
diff --git a/tgstation.dme b/tgstation.dme
index 1d5ca6537e9..a4d6a34c688 100644
--- a/tgstation.dme
+++ b/tgstation.dme
@@ -6400,6 +6400,7 @@
#include "code\modules\vending\liberation.dm"
#include "code\modules\vending\liberation_toy.dm"
#include "code\modules\vending\magivend.dm"
+#include "code\modules\vending\mail.dm"
#include "code\modules\vending\medical.dm"
#include "code\modules\vending\medical_wall.dm"
#include "code\modules\vending\megaseed.dm"