diff --git a/code/_helpers/unsorted_vr.dm b/code/_helpers/unsorted_vr.dm
new file mode 100644
index 0000000000..e9b5f86f0b
--- /dev/null
+++ b/code/_helpers/unsorted_vr.dm
@@ -0,0 +1,10 @@
+/*
+ get_holder_at_turf_level(): Similar to get_turf(), will return the "highest up" holder of this atom, excluding the turf.
+ Example: A fork inside a box inside a locker will return the locker. Essentially, get_just_before_turf().
+*/ //Credit to /vg/
+/proc/get_holder_at_turf_level(const/atom/movable/O)
+ if(!istype(O)) //atom/movable does not include areas
+ return
+ var/atom/A
+ for(A=O, A && !isturf(A.loc), A=A.loc); // semicolon is for the empty statement
+ return A
\ No newline at end of file
diff --git a/code/game/objects/mob_spawner_vr.dm b/code/game/objects/mob_spawner_vr.dm
new file mode 100644
index 0000000000..c1d0ba1ec1
--- /dev/null
+++ b/code/game/objects/mob_spawner_vr.dm
@@ -0,0 +1,105 @@
+/obj/structure/mob_spawner
+ name = "mob spawner"
+ desc = "This shouldn't be seen, yell at a dev."
+ icon = 'icons/effects/effects.dmi'
+ icon_state = "rift"
+ anchored = 1
+
+ var/last_spawn = 0
+ var/spawn_delay = 10 MINUTES
+
+ var/list/spawn_types = list(
+ /mob/living/simple_animal/corgi = 100,
+ /mob/living/simple_animal/cat = 25
+ )
+
+ var/total_spawns = -1 //Total mob spawns, over all time, -1 for no limit
+ var/simultaneous_spawns = 3 //Max spawned mobs active at one time
+ var/mob_faction
+
+ var/destructable = 0
+ var/health = 50
+
+ var/list/spawned_mobs = list()
+
+/obj/structure/mob_spawner/initialize()
+ ..()
+ processing_objects.Add(src)
+ last_spawn = world.time + rand(0,spawn_delay)
+
+/obj/structure/mob_spawner/Destroy()
+ processing_objects.Remove(src)
+ for(var/mob/living/L in spawned_mobs)
+ L.source_spawner = null
+ spawned_mobs.Cut()
+ return ..()
+
+/obj/structure/mob_spawner/process()
+ if(!can_spawn())
+ return
+ var/chosen_mob = choose_spawn()
+ if(chosen_mob)
+ do_spawn(chosen_mob)
+
+/obj/structure/mob_spawner/proc/can_spawn()
+ if(!total_spawns)
+ return 0
+ if(spawned_mobs.len >= simultaneous_spawns)
+ return 0
+ if(world.time < last_spawn + spawn_delay)
+ return 0
+ return 1
+
+/obj/structure/mob_spawner/proc/choose_spawn()
+ return pickweight(spawn_types)
+
+/obj/structure/mob_spawner/proc/do_spawn(var/mob_path)
+ if(!ispath(mob_path))
+ return 0
+ var/mob/living/L = new mob_path(get_turf(src))
+ L.source_spawner = src
+ spawned_mobs.Add(L)
+ last_spawn = world.time
+ if(total_spawns > 0)
+ total_spawns--
+ if(mob_faction)
+ L.faction = mob_faction
+ return L
+
+/obj/structure/mob_spawner/proc/get_death_report(var/mob/living/L)
+ if(L in spawned_mobs)
+ spawned_mobs.Remove(L)
+
+/obj/structure/mob_spawner/attackby(var/obj/item/I, var/mob/living/user)
+ if(!I.force || I.flags & NOBLUDGEON || !destructable)
+ return
+
+ user.setClickCooldown(DEFAULT_ATTACK_COOLDOWN)
+ user.do_attack_animation(src)
+ visible_message("\The [src] has been [I.attack_verb.len ? "[pick(I.attack_verb)]":"attacked"] with \the [I] by [user].")
+ take_damage(I.force)
+
+/obj/structure/mob_spawner/bullet_act(var/obj/item/projectile/Proj)
+ ..()
+ take_damage(Proj.get_structure_damage())
+
+/obj/structure/mob_spawner/proc/take_damage(var/damage)
+ health -= damage
+ if(health <= 0)
+ visible_message("\The [src] breaks apart!")
+ qdel(src)
+
+/obj/structure/mob_spawner/clear_zlevel/can_spawn()
+ if(!..())
+ return 0
+ var/turf/T = get_turf(src)
+ if(!T)
+ return 0
+ for(var/mob/living/L in player_list)
+ var/turf/L_T
+ if(L.stat == DEAD)
+ continue
+ L_T = get_turf(L)
+ if(T.z == L_T.z)
+ return 0
+ return 1
\ No newline at end of file
diff --git a/code/game/objects/structures/trash_pile.dm b/code/game/objects/structures/trash_pile.dm
index f4a7d146d2..7f3a6792b2 100644
--- a/code/game/objects/structures/trash_pile.dm
+++ b/code/game/objects/structures/trash_pile.dm
@@ -9,6 +9,8 @@
var/list/searchedby = list()// Characters that have searched this trashpile, with values of searched time.
var/mob/living/hider // A simple animal that might be hiding in the pile
+ var/obj/structure/mob_spawner/mouse_nest/mouse_nest = null
+
var/chance_alpha = 79 // Alpha list is junk items and normal random stuff.
var/chance_beta = 20 // Beta list is actually maybe some useful illegal items. If it's not alpha or gamma, it's beta.
var/chance_gamma = 1 // Gamma list is unique items only, and will only spawn one of each. This is a sub-chance of beta chance.
@@ -40,6 +42,12 @@
"boxfort",
"trashbag",
"brokecomp")
+ mouse_nest = new(src)
+
+/obj/structure/trash_pile/Destroy()
+ qdel(mouse_nest)
+ mouse_nest = null
+ return ..()
/obj/structure/trash_pile/attackby(obj/item/W as obj, mob/user as mob)
var/w_type = W.type
@@ -252,3 +260,30 @@
else
return produce_beta_item()
+/obj/structure/mob_spawner/mouse_nest
+ name = "trash"
+ desc = "A small heap of trash, perfect for mice to nest in."
+ icon = 'icons/obj/trash_piles.dmi'
+ icon_state = "randompile"
+ spawn_types = list(/mob/living/simple_animal/mouse)
+ simultaneous_spawns = 2
+
+/obj/structure/mob_spawner/mouse_nest/initialize()
+ ..()
+ icon_state = pick(
+ "pile1",
+ "pile2",
+ "pilechair",
+ "piletable",
+ "pilevending",
+ "brtrashpile",
+ "microwavepile",
+ "rackpile",
+ "boxfort",
+ "trashbag",
+ "brokecomp")
+
+/obj/structure/mob_spawner/mouse_nest/do_spawn(var/mob_path)
+ . = ..()
+ var/atom/A = get_holder_at_turf_level(src)
+ A.visible_message("[.] crawls out of \the [src].")
\ No newline at end of file
diff --git a/code/modules/mob/living/death.dm b/code/modules/mob/living/death.dm
index d6c2bc0405..9ea12b967c 100644
--- a/code/modules/mob/living/death.dm
+++ b/code/modules/mob/living/death.dm
@@ -1,3 +1,8 @@
/mob/living/death()
clear_fullscreens()
+ //VOREStation Edit - Mob spawner stuff
+ if(source_spawner)
+ source_spawner.get_death_report(src)
+ source_spawner = null
+ //VOREStation Edit End
. = ..()
\ No newline at end of file
diff --git a/code/modules/mob/living/living_defines_vr.dm b/code/modules/mob/living/living_defines_vr.dm
index 566c21dad8..8b463df93b 100644
--- a/code/modules/mob/living/living_defines_vr.dm
+++ b/code/modules/mob/living/living_defines_vr.dm
@@ -1,2 +1,3 @@
/mob/living
- var/ooc_notes = null
\ No newline at end of file
+ var/ooc_notes = null
+ var/obj/structure/mob_spawner/source_spawner = null
\ No newline at end of file
diff --git a/vorestation.dme b/vorestation.dme
index b0ac82c068..b36a95c38d 100644
--- a/vorestation.dme
+++ b/vorestation.dme
@@ -84,6 +84,7 @@
#include "code\_helpers\type2type.dm"
#include "code\_helpers\type2type_vr.dm"
#include "code\_helpers\unsorted.dm"
+#include "code\_helpers\unsorted_vr.dm"
#include "code\_helpers\vector.dm"
#include "code\_helpers\sorts\__main.dm"
#include "code\_helpers\sorts\comparators.dm"
@@ -783,6 +784,7 @@
#include "code\game\objects\explosion.dm"
#include "code\game\objects\explosion_recursive.dm"
#include "code\game\objects\items.dm"
+#include "code\game\objects\mob_spawner_vr.dm"
#include "code\game\objects\objs.dm"
#include "code\game\objects\structures.dm"
#include "code\game\objects\stumble_into_vr.dm"