diff --git a/code/_macros.dm b/code/_macros.dm
index e8a97cbada..6c1f37b4bc 100644
--- a/code/_macros.dm
+++ b/code/_macros.dm
@@ -90,6 +90,9 @@
#define GENERATE_DEBUG_ID "[rand(0, 9)][rand(0, 9)][rand(0, 9)][rand(0, 9)][pick(alphabet_lowercase)][pick(alphabet_lowercase)][pick(alphabet_lowercase)][pick(alphabet_lowercase)]"
#define RECT new /datum/shape/rectangle
+#define SQUARE new /datum/shape/rectangle/square
+#define ELLIPSE new /datum/shape/ellipse
+#define CIRCLE new /datum/shape/ellipse/circle
#define QTREE new /datum/quadtree
#define SEARCH_QTREE(qtree, shape_range, flags) qtree.query_range(shape_range, null, flags)
diff --git a/code/controllers/subsystem/sound.dm b/code/controllers/subsystem/sound.dm
index 4fdfd79353..010850dda2 100644
--- a/code/controllers/subsystem/sound.dm
+++ b/code/controllers/subsystem/sound.dm
@@ -19,8 +19,7 @@ SUBSYSTEM_DEF(sound)
if(!run_hearers) // Initialize for handling next template
run_hearers = run_queue[run_template] // get base hearers
if(run_template.range) // ranging
- var/datum/shape/rectangle/zone = RECT(run_template.x, run_template.y, run_template.range * 2, run_template.range * 2)
- run_hearers |= SSquadtree.players_in_range(zone, run_template.z)
+ run_hearers |= SSquadtree.players_in_range(SQUARE(run_template.x, run_template.y, run_template.range * 2), run_template.z)
if(MC_TICK_CHECK)
return
while(length(run_hearers)) // Output sound to hearers
diff --git a/code/controllers/subsystem/techtree.dm b/code/controllers/subsystem/techtree.dm
index 04ac2591bc..5f22373228 100644
--- a/code/controllers/subsystem/techtree.dm
+++ b/code/controllers/subsystem/techtree.dm
@@ -34,17 +34,6 @@ SUBSYSTEM_DEF(techtree)
var/datum/space_level/zpos = SSmapping.add_new_zlevel(tree.name, list(ZTRAIT_TECHTREE))
tree.zlevel = zpos
- var/zlevel = zpos.z_value
- var/turf/z_min = locate(1, 1, zlevel)
- var/turf/z_max = locate(world.maxx, world.maxy, zlevel)
-
-
-
- for(var/t in block(z_min, z_max))
- var/turf/Tu = t
- Tu.ChangeTurf(/turf/closed/void, list(/turf/closed/void))
- new /area/techtree(Tu)
-
for(var/tier in tree.tree_tiers)
tree.unlocked_techs += tier
tree.all_techs += tier
diff --git a/code/controllers/subsystem/ticker.dm b/code/controllers/subsystem/ticker.dm
index e06e1ac458..ef796bc439 100644
--- a/code/controllers/subsystem/ticker.dm
+++ b/code/controllers/subsystem/ticker.dm
@@ -109,14 +109,6 @@ SUBSYSTEM_DEF(ticker)
mode.declare_completion(force_ending)
REDIS_PUBLISH("byond.round", "type" = "round-complete")
flash_clients()
- addtimer(CALLBACK(
- SSvote,
- /datum/controller/subsystem/vote/proc/initiate_vote,
- "gamemode",
- "SERVER",
- CALLBACK(src, PROC_REF(handle_map_reboot)),
- TRUE
- ), 3 SECONDS)
Master.SetRunLevel(RUNLEVEL_POSTGAME)
/// Attempt to start game asynchronously if applicable
@@ -161,16 +153,6 @@ SUBSYSTEM_DEF(ticker)
return FALSE
return TRUE
-/datum/controller/subsystem/ticker/proc/handle_map_reboot()
- addtimer(CALLBACK(
- SSvote,
- /datum/controller/subsystem/vote/proc/initiate_vote,
- "groundmap",
- "SERVER",
- CALLBACK(src, PROC_REF(Reboot)),
- TRUE
- ), 3 SECONDS)
-
/datum/controller/subsystem/ticker/proc/setup()
to_chat(world, SPAN_BOLDNOTICE("Enjoy the game!"))
var/init_start = world.timeofday
@@ -349,40 +331,6 @@ SUBSYSTEM_DEF(ticker)
fdel("data/mode.txt")
WRITE_FILE(file("data/mode.txt"), the_mode)
-
-/datum/controller/subsystem/ticker/proc/Reboot(reason, delay)
- set waitfor = FALSE
-
- if(usr && !check_rights(R_SERVER))
- return
-
- if(graceful)
- to_chat_forced(world, "
[SPAN_BOLDNOTICE("Shutting down...")]
")
- world.Reboot(FALSE)
- return
-
- if(!delay)
- delay = CONFIG_GET(number/round_end_countdown) * 10
-
- var/skip_delay = check_rights()
- if(delay_end && !skip_delay)
- to_chat(world, SPAN_BOLDNOTICE("An admin has delayed the round end."))
- return
-
- to_chat(world, SPAN_BOLDNOTICE("Rebooting World in [DisplayTimeText(delay)]. [reason]"))
-
- var/start_wait = world.time
- sleep(delay - (world.time - start_wait))
-
- if(delay_end && !skip_delay)
- to_chat(world, SPAN_BOLDNOTICE("Reboot was cancelled by an admin."))
- return
-
- log_game("Rebooting World. [reason]")
- to_chat_forced(world, "[SPAN_BOLDNOTICE("Rebooting...")]
")
-
- world.Reboot(TRUE)
-
/datum/controller/subsystem/ticker/proc/create_characters()
if(!RoleAuthority)
return
diff --git a/code/datums/quadtree.dm b/code/datums/quadtree.dm
index bf5cd8000b..ec3e577a24 100644
--- a/code/datums/quadtree.dm
+++ b/code/datums/quadtree.dm
@@ -49,43 +49,124 @@
..()
return QDEL_HINT_IWILLGC
-/datum/shape //Leaving rectangles as a subtype if anyone decides to add circles later
+/// A simple geometric shape for testing collisions and intersections. This one is a single point.
+/datum/shape
+ /// Horizontal position of the shape's center point.
var/center_x = 0
+ /// Vertical position of the shape's center point.
var/center_y = 0
+ /// Distance from the shape's leftmost to rightmost extent.
+ var/bounds_x = 0
+ /// Distance from the shape's topmost to bottommost extent.
+ var/bounds_y = 0
-/datum/shape/proc/intersects()
- return
-/datum/shape/proc/contains()
- return
+/datum/shape/New(center_x, center_y)
+ set_shape(center_x, center_y)
+/// Assign shape variables.
+/datum/shape/proc/set_shape(center_x, center_y)
+ src.center_x = center_x
+ src.center_y = center_y
+
+/// Returns TRUE if the coordinates x, y are in or on the shape, otherwise FALSE.
+/datum/shape/proc/contains_xy(x, y)
+ return center_x == x && center_y == y
+
+/// Returns TRUE if the coord datum is in or on the shape, otherwise FALSE.
+/datum/shape/proc/contains_coords(datum/coords/coords)
+ return contains_xy(coords.x_pos, coords.y_pos)
+
+/// Returns TRUE if the atom is in or on the shape, otherwise FALSE.
+/datum/shape/proc/contains_atom(atom/atom)
+ return contains_xy(atom.x, atom.y)
+
+/// Returns TRUE if this shape's bounding box intersects the provided shape's bounding box, otherwise FALSE. Generally faster than a full intersection test.
+/datum/shape/proc/intersects_aabb(datum/shape/aabb)
+ return (abs(src.center_x - aabb.center_x) <= (src.bounds_x + aabb.bounds_x) * 0.5) && (abs(src.center_y - aabb.center_y) <= (src.bounds_y + aabb.bounds_y) * 0.5)
+
+/// Returns TRUE if this shape intersects the provided rectangle shape, otherwise FALSE.
+/datum/shape/proc/intersects_rect(datum/shape/rectangle/rect)
+ return rect.contains_xy(src.center_x, src.center_y)
+
+/// A simple geometric shape for testing collisions and intersections. This one is an axis-aligned rectangle.
/datum/shape/rectangle
+ /// Distance from the shape's leftmost to rightmost extent.
+ var/width = 0
+ /// Distance from the shape's topmost to bottommost extent.
+ var/height = 0
+
+/datum/shape/rectangle/New(center_x, center_y, width, height)
+ set_shape(center_x, center_y, width, height)
+
+/datum/shape/rectangle/set_shape(center_x, center_y, width, height)
+ ..()
+ src.bounds_x = width
+ src.bounds_y = height
+ src.width = width
+ src.height = height
+
+/datum/shape/rectangle/contains_xy(x, y)
+ return (abs(center_x - x) <= width * 0.5) && (abs(center_y - y) <= height * 0.5)
+
+/datum/shape/rectangle/intersects_rect(datum/shape/rectangle/rect)
+ return intersects_aabb(rect)
+
+/// A simple geometric shape for testing collisions and intersections. This one is an axis-aligned square.
+/datum/shape/rectangle/square
+ /// Distance between the shape's opposing extents.
+ var/length = 0
+
+/datum/shape/rectangle/square/New(center_x, center_y, length)
+ set_shape(center_x, center_y, length)
+
+/datum/shape/rectangle/square/set_shape(center_x, center_y, length)
+ ..(center_x, center_y, length, length)
+ src.length = length
+
+/// A simple geometric shape for testing collisions and intersections. This one is an axis-aligned ellipse.
+/datum/shape/ellipse
+ /// Distance from the shape's leftmost to rightmost extent.
var/width = 0
+ /// Distance from the shape's topmost to bottommost extent.
var/height = 0
+ VAR_PROTECTED/_axis_x_sq = 0
+ VAR_PROTECTED/_axis_y_sq = 0
+
+/datum/shape/ellipse/New(center_x, center_y, width, height)
+ set_shape(center_x, center_y, width, height)
-/datum/shape/rectangle/New(x, y, w, h)
+/datum/shape/ellipse/set_shape(center_x, center_y, width, height)
..()
- center_x = x
- center_y = y
- width = w
- height = h
-
-/datum/shape/rectangle/intersects(datum/shape/rectangle/range)
- return !(range.center_x + range.width/2 < center_x - width / 2|| \
- range.center_x - range.width/2 > center_x + width / 2|| \
- range.center_y + range.height/2 < center_y - height / 2|| \
- range.center_y - range.height/2 > center_y + height / 2)
-
-/datum/shape/rectangle/contains(datum/coords/coords)
- return (coords.x_pos >= center_x - width / 2 \
- && coords.x_pos <= center_x + width / 2 \
- && coords.y_pos >= center_y - height /2 \
- && coords.y_pos <= center_y + height / 2)
-
-/datum/shape/rectangle/proc/contains_atom(atom/A)
- return (A.x >= center_x - width / 2 \
- && A.x <= center_x + width / 2 \
- && A.y >= center_y - height /2 \
- && A.y <= center_y + height / 2)
+ src.bounds_x = width
+ src.bounds_y = height
+ src.width = width
+ src.height = height
+ src._axis_x_sq = (width * 0.5)**2
+ src._axis_y_sq = (height * 0.5)**2
+
+/datum/shape/ellipse/contains_xy(x, y)
+ return ((center_x - x)**2 / _axis_x_sq + (center_y - y)**2 / _axis_y_sq <= 1)
+
+/datum/shape/ellipse/intersects_rect(datum/shape/rectangle/rect)
+ if(..())
+ return TRUE
+
+ var/nearest_x = clamp(src.center_x, rect.center_x - rect.width * 0.5, rect.center_x + rect.width * 0.5)
+ var/nearest_y = clamp(src.center_y, rect.center_y - rect.height * 0.5, rect.center_y + rect.height * 0.5)
+
+ return src.contains_xy(nearest_x, nearest_y)
+
+/// A simple geometric shape for testing collisions and intersections. This one is a circle.
+/datum/shape/ellipse/circle
+ /// Distance from the shape's center to edge.
+ var/radius = 0
+
+/datum/shape/ellipse/circle/New(center_x, center_y, radius)
+ set_shape(center_x, center_y, radius)
+
+/datum/shape/ellipse/circle/set_shape(center_x, center_y, radius)
+ ..(center_x, center_y, radius * 2, radius * 2)
+ src.radius = radius
/datum/quadtree/proc/subdivide()
//Warning: this might give you eye cancer
@@ -96,7 +177,7 @@
is_divided = TRUE
/datum/quadtree/proc/insert_player(datum/coords/qtplayer/p_coords)
- if(!boundary.contains(p_coords))
+ if(!boundary.contains_coords(p_coords))
return FALSE
if(!player_coords)
@@ -118,11 +199,11 @@
player_coords.Add(p_coords)
return TRUE
-/datum/quadtree/proc/query_range(datum/shape/rectangle/range, list/found_players, flags = 0)
+/datum/quadtree/proc/query_range(datum/shape/range, list/found_players, flags = 0)
if(!found_players)
found_players = list()
. = found_players
- if(!range?.intersects(boundary))
+ if(!range?.intersects_rect(boundary))
return
if(is_divided)
nw_branch.query_range(range, found_players, flags)
@@ -136,7 +217,7 @@
continue
if((flags & QTREE_EXCLUDE_OBSERVER) && P.is_observer)
continue
- if(range.contains(P))
+ if(range.contains_coords(P))
if(flags & QTREE_SCAN_MOBS)
found_players.Add(P.player)
continue
diff --git a/code/game/objects/items/devices/motion_detector.dm b/code/game/objects/items/devices/motion_detector.dm
index 99b11d1ff6..60cf62b4e2 100644
--- a/code/game/objects/items/devices/motion_detector.dm
+++ b/code/game/objects/items/devices/motion_detector.dm
@@ -37,7 +37,7 @@
var/iff_signal = FACTION_MARINE
actions_types = list(/datum/action/item_action)
var/scanning = FALSE // controls if MD is in process of scan
- var/datum/shape/rectangle/range_bounds
+ var/datum/shape/rectangle/square/range_bounds
var/long_range_locked = FALSE //only long-range MD
var/ping_overlay
@@ -53,7 +53,7 @@
/obj/item/device/motiondetector/Initialize()
. = ..()
- range_bounds = new //Just creating a rectangle datum
+ range_bounds = new //Just creating a square datum
update_icon()
/obj/item/device/motiondetector/Destroy()
@@ -215,12 +215,7 @@
if(!istype(cur_turf))
return
- if(!range_bounds)
- range_bounds = new/datum/shape/rectangle
- range_bounds.center_x = cur_turf.x
- range_bounds.center_y = cur_turf.y
- range_bounds.width = detector_range * 2
- range_bounds.height = detector_range * 2
+ range_bounds.set_shape(cur_turf.x, cur_turf.y, detector_range * 2)
var/list/ping_candidates = SSquadtree.players_in_range(range_bounds, cur_turf.z, QTREE_EXCLUDE_OBSERVER | QTREE_SCAN_MOBS)
diff --git a/code/game/objects/structures/fence.dm b/code/game/objects/structures/fence.dm
index b29c69e8af..6a4b479929 100644
--- a/code/game/objects/structures/fence.dm
+++ b/code/game/objects/structures/fence.dm
@@ -3,6 +3,7 @@
desc = "A large metal mesh strewn between two poles. Intended as a cheap way to separate areas, while allowing one to see through it."
icon = 'icons/obj/structures/props/fence.dmi'
icon_state = "fence0"
+ throwpass = TRUE
density = TRUE
anchored = TRUE
layer = WINDOW_LAYER
diff --git a/code/modules/admin/banjob.dm b/code/modules/admin/banjob.dm
index 18f06e79a6..230590fdf9 100644
--- a/code/modules/admin/banjob.dm
+++ b/code/modules/admin/banjob.dm
@@ -146,6 +146,12 @@ WARNING!*/
else
jobs += "Emergency Response Team | "
+ //Freed Mobs
+ if(jobban_isbanned(M, "Freed Mob", P) || isbanned_dept)
+ jobs += "Freed Mob | "
+ else
+ jobs += "Freed Mob | "
+
//Survivor
if(jobban_isbanned(M, "Survivor", P) || isbanned_dept)
jobs += "Survivor | "
diff --git a/code/modules/cm_aliens/structures/special/egg_morpher.dm b/code/modules/cm_aliens/structures/special/egg_morpher.dm
index 1fd154eb35..e8040f81ba 100644
--- a/code/modules/cm_aliens/structures/special/egg_morpher.dm
+++ b/code/modules/cm_aliens/structures/special/egg_morpher.dm
@@ -14,14 +14,14 @@
var/huggers_to_grow_max = 12
var/huggers_reserved = 0
var/mob/captured_mob
- var/datum/shape/rectangle/range_bounds
+ var/datum/shape/range_bounds
appearance_flags = KEEP_TOGETHER
layer = FACEHUGGER_LAYER
/obj/effect/alien/resin/special/eggmorph/Initialize(mapload, hive_ref)
. = ..()
- range_bounds = RECT(x, y, EGGMORPG_RANGE, EGGMORPG_RANGE)
+ range_bounds = SQUARE(x, y, EGGMORPG_RANGE)
/obj/effect/alien/resin/special/eggmorph/Destroy()
if (stored_huggers && linked_hive)
@@ -155,7 +155,7 @@
/obj/effect/alien/resin/special/eggmorph/proc/check_facehugger_target()
if(!range_bounds)
- range_bounds = RECT(x, y, EGGMORPG_RANGE, EGGMORPG_RANGE)
+ range_bounds = SQUARE(x, y, EGGMORPG_RANGE)
var/list/targets = SSquadtree.players_in_range(range_bounds, z, QTREE_SCAN_MOBS | QTREE_EXCLUDE_OBSERVER)
if(isnull(targets) || !length(targets))
diff --git a/code/modules/cm_tech/techtree.dm b/code/modules/cm_tech/techtree.dm
index 6c39d8ab9c..a027789185 100644
--- a/code/modules/cm_tech/techtree.dm
+++ b/code/modules/cm_tech/techtree.dm
@@ -58,16 +58,24 @@
if(longest_tier < tier_length)
longest_tier = tier_length
- // Clear out the area
- for(var/t in block(locate(1, 1, zlevel.z_value), locate(longest_tier * 2 + 1, length(all_techs) * 3 + 1, zlevel.z_value)))
+ // Clear out and create the area
+ // (The `+ 2` on both of these is 1 for a buffer tile, and 1 for the outer `/turf/closed/void`.)
+ var/area_max_x = longest_tier * 2 + 2
+ var/area_max_y = length(all_techs) * 3 + 2
+ for(var/t in block(locate(1, 1, zlevel.z_value), locate(area_max_x, area_max_y, zlevel.z_value)))
var/turf/pos = t
for(var/A in pos)
qdel(A)
- pos.ChangeTurf(/turf/open/blank)
- pos.color = "#000000"
-
+ if(pos.x == area_max_x || pos.y == area_max_y)
+ // The turfs around the edge are closed.
+ pos.ChangeTurf(/turf/closed/void)
+ else
+ pos.ChangeTurf(/turf/open/blank)
+ pos.color = "#000000"
+ new /area/techtree(pos)
+ // Create the tech nodes
var/y_offset = 1
for(var/tier in all_techs)
var/tier_length = length(all_techs[tier])
diff --git a/code/modules/defenses/bell_tower.dm b/code/modules/defenses/bell_tower.dm
index 52207298c4..a9d6c08bc7 100644
--- a/code/modules/defenses/bell_tower.dm
+++ b/code/modules/defenses/bell_tower.dm
@@ -257,7 +257,7 @@
STOP_PROCESSING(SSobj, src)
return
- var/list/targets = SSquadtree.players_in_range(RECT(M.x, M.y, area_range, area_range), M.z, QTREE_SCAN_MOBS | QTREE_EXCLUDE_OBSERVER)
+ var/list/targets = SSquadtree.players_in_range(SQUARE(M.x, M.y, area_range), M.z, QTREE_SCAN_MOBS | QTREE_EXCLUDE_OBSERVER)
if(!targets)
return
diff --git a/code/modules/defenses/planted_flag.dm b/code/modules/defenses/planted_flag.dm
index fac725047f..f0f6b05565 100644
--- a/code/modules/defenses/planted_flag.dm
+++ b/code/modules/defenses/planted_flag.dm
@@ -7,7 +7,7 @@
desc = "A planted flag with the iconic USCM flag plastered all over it, you feel a burst of energy by its mere sight."
handheld_type = /obj/item/defenses/handheld/planted_flag
disassemble_time = 10
- var/datum/shape/rectangle/range_bounds
+ var/datum/shape/range_bounds
var/area_range = PLANTED_FLAG_RANGE
var/buff_intensity = PLANTED_FLAG_BUFF
health = 200
@@ -33,7 +33,7 @@
apply_area_effect()
start_processing()
- range_bounds = RECT(x, y, PLANTED_FLAG_RANGE, PLANTED_FLAG_RANGE)
+ range_bounds = SQUARE(x, y, PLANTED_FLAG_RANGE)
update_icon()
/obj/structure/machinery/defenses/planted_flag/Destroy()
@@ -70,9 +70,9 @@
/obj/structure/machinery/defenses/planted_flag/proc/apply_area_effect()
if(!range_bounds)
- range_bounds = RECT(x, y, area_range, area_range)
+ range_bounds = SQUARE(x, y, area_range)
- var/list/targets = SSquadtree.players_in_range(RECT(x, y, area_range, area_range), z, QTREE_SCAN_MOBS | QTREE_EXCLUDE_OBSERVER)
+ var/list/targets = SSquadtree.players_in_range(SQUARE(x, y, area_range), z, QTREE_SCAN_MOBS | QTREE_EXCLUDE_OBSERVER)
if(!targets)
return
@@ -146,7 +146,7 @@
if(!M.x && !M.y && !M.z)
return
- var/list/targets = SSquadtree.players_in_range(RECT(M.x, M.y, area_range, area_range), M.z, QTREE_SCAN_MOBS | QTREE_EXCLUDE_OBSERVER)
+ var/list/targets = SSquadtree.players_in_range(SQUARE(M.x, M.y, area_range), M.z, QTREE_SCAN_MOBS | QTREE_EXCLUDE_OBSERVER)
targets |= M
for(var/mob/living/carbon/human/H in targets)
diff --git a/code/modules/mob/dead/observer/observer.dm b/code/modules/mob/dead/observer/observer.dm
index 44a3592329..409c88388f 100644
--- a/code/modules/mob/dead/observer/observer.dm
+++ b/code/modules/mob/dead/observer/observer.dm
@@ -1034,6 +1034,10 @@ This is the proc mobs get to turn into a ghost. Forked from ghostize due to comp
to_chat(src, SPAN_WARNING("The game hasn't started yet!"))
return
+ if(jobban_isbanned(src, "Freed Mob"))
+ to_chat(src, SPAN_WARNING("You are banned from being able to join as a freed mob."))
+ return
+
var/list/mobs_by_role = list() // the list the mobs are assigned to first, for sorting purposes
for(var/mob/freed_mob as anything in GLOB.freed_mob_list)
var/role_name = freed_mob.get_role_name()
@@ -1059,6 +1063,10 @@ This is the proc mobs get to turn into a ghost. Forked from ghostize due to comp
if(!istype(freed_mob) || !(freed_mob in GLOB.freed_mob_list))
return
+ if(jobban_isbanned(src, "Freed Mob"))
+ to_chat(src, SPAN_WARNING("You are banned from being able to join as a freed mob."))
+ return
+
if(QDELETED(freed_mob) || freed_mob.client)
GLOB.freed_mob_list -= freed_mob
to_chat(src, SPAN_WARNING("Something went wrong."))
diff --git a/html/changelogs/archive/2024-07.yml b/html/changelogs/archive/2024-07.yml
index 4042cde8ee..f171a09f14 100644
--- a/html/changelogs/archive/2024-07.yml
+++ b/html/changelogs/archive/2024-07.yml
@@ -5,3 +5,12 @@
Doubleumc:
- admin: Radio Clarity slider now shows an example message
- qol: ARC turret shots start from the center of the ARC
+2024-07-24:
+ Doubleumc:
+ - bugfix: sounds & motion detectors should be more reliable
+ - rscdel: Removed automatic end of round vote and reboot
+2024-07-25:
+ Doubleumc:
+ - code_imp: Made the Tech Tree subsystem initialise faster.
+ Zonespace27:
+ - admin: Added freed mob bans to the jobban panel.