From 9e16c4440bc335fcfd2cf28e6854c32c87bb4eaa Mon Sep 17 00:00:00 2001
From: Zonespace <41448081+Zonespace27@users.noreply.github.com>
Date: Wed, 24 Jul 2024 16:20:01 -0700
Subject: [PATCH 001/140] Adds freed mob jobban (#345)
---
code/modules/admin/banjob.dm | 6 ++++++
code/modules/mob/dead/observer/observer.dm | 8 ++++++++
2 files changed, 14 insertions(+)
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/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."))
From f7166dceed51217b94f586d4746da53c25d15495 Mon Sep 17 00:00:00 2001
From: KoishiVibe <111302138+KoishiVibe@users.noreply.github.com>
Date: Wed, 24 Jul 2024 18:20:12 -0500
Subject: [PATCH 002/140] Port-pvp-fixes-2 (#344)
Co-authored-by: KoishiVibe
---
code/game/objects/structures/fence.dm | 1 +
1 file changed, 1 insertion(+)
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
From ceac4ab1903a40268726f1c70ee27a8980e8f0f1 Mon Sep 17 00:00:00 2001
From: cm13-github <128137806+cm13-github@users.noreply.github.com>
Date: Thu, 25 Jul 2024 00:20:18 +0100
Subject: [PATCH 003/140] Automatic changelog for PR #345 [ci skip]
---
html/changelogs/AutoChangeLog-pr-345.yml | 4 ++++
1 file changed, 4 insertions(+)
create mode 100644 html/changelogs/AutoChangeLog-pr-345.yml
diff --git a/html/changelogs/AutoChangeLog-pr-345.yml b/html/changelogs/AutoChangeLog-pr-345.yml
new file mode 100644
index 0000000000..8a315c2f78
--- /dev/null
+++ b/html/changelogs/AutoChangeLog-pr-345.yml
@@ -0,0 +1,4 @@
+author: "Zonespace27"
+delete-after: True
+changes:
+ - admin: "Added freed mob bans to the jobban panel."
\ No newline at end of file
From 1c09c053ab3cbd6eb07cc764033fe8830c4ae807 Mon Sep 17 00:00:00 2001
From: Doubleumc
Date: Wed, 24 Jul 2024 19:20:24 -0400
Subject: [PATCH 004/140] Port "SStechtree startup optimisation" (#343)
Co-authored-by: SabreML <57483089+SabreML@users.noreply.github.com>
---
code/controllers/subsystem/techtree.dm | 11 -----------
code/modules/cm_tech/techtree.dm | 18 +++++++++++++-----
2 files changed, 13 insertions(+), 16 deletions(-)
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/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])
From 8517546a57931a1bc41be78d73716734af730f44 Mon Sep 17 00:00:00 2001
From: cm13-github <128137806+cm13-github@users.noreply.github.com>
Date: Thu, 25 Jul 2024 00:23:51 +0100
Subject: [PATCH 005/140] Automatic changelog for PR #343 [ci skip]
---
html/changelogs/AutoChangeLog-pr-343.yml | 4 ++++
1 file changed, 4 insertions(+)
create mode 100644 html/changelogs/AutoChangeLog-pr-343.yml
diff --git a/html/changelogs/AutoChangeLog-pr-343.yml b/html/changelogs/AutoChangeLog-pr-343.yml
new file mode 100644
index 0000000000..750b27e47e
--- /dev/null
+++ b/html/changelogs/AutoChangeLog-pr-343.yml
@@ -0,0 +1,4 @@
+author: "Doubleumc"
+delete-after: True
+changes:
+ - code_imp: "Made the Tech Tree subsystem initialise faster."
\ No newline at end of file
From c3742c9dbaeb986d5e4c1e06064a2379241bd05b Mon Sep 17 00:00:00 2001
From: Changelogs
Date: Thu, 25 Jul 2024 01:16:20 +0000
Subject: [PATCH 006/140] Automatic changelog compile [ci skip]
---
html/changelogs/AutoChangeLog-pr-343.yml | 4 ----
html/changelogs/AutoChangeLog-pr-345.yml | 4 ----
html/changelogs/archive/2024-07.yml | 5 +++++
3 files changed, 5 insertions(+), 8 deletions(-)
delete mode 100644 html/changelogs/AutoChangeLog-pr-343.yml
delete mode 100644 html/changelogs/AutoChangeLog-pr-345.yml
diff --git a/html/changelogs/AutoChangeLog-pr-343.yml b/html/changelogs/AutoChangeLog-pr-343.yml
deleted file mode 100644
index 750b27e47e..0000000000
--- a/html/changelogs/AutoChangeLog-pr-343.yml
+++ /dev/null
@@ -1,4 +0,0 @@
-author: "Doubleumc"
-delete-after: True
-changes:
- - code_imp: "Made the Tech Tree subsystem initialise faster."
\ No newline at end of file
diff --git a/html/changelogs/AutoChangeLog-pr-345.yml b/html/changelogs/AutoChangeLog-pr-345.yml
deleted file mode 100644
index 8a315c2f78..0000000000
--- a/html/changelogs/AutoChangeLog-pr-345.yml
+++ /dev/null
@@ -1,4 +0,0 @@
-author: "Zonespace27"
-delete-after: True
-changes:
- - admin: "Added freed mob bans to the jobban panel."
\ No newline at end of file
diff --git a/html/changelogs/archive/2024-07.yml b/html/changelogs/archive/2024-07.yml
index a8046763ac..f171a09f14 100644
--- a/html/changelogs/archive/2024-07.yml
+++ b/html/changelogs/archive/2024-07.yml
@@ -9,3 +9,8 @@
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.
From e8ca694f8f8bad216ede14baad99d2419d9f0693 Mon Sep 17 00:00:00 2001
From: Doubleumc
Date: Mon, 29 Jul 2024 22:29:43 -0400
Subject: [PATCH 007/140] Round Start delayed by default (#351)
---
code/controllers/subsystem/ticker.dm | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/code/controllers/subsystem/ticker.dm b/code/controllers/subsystem/ticker.dm
index ef796bc439..cfe66421c9 100644
--- a/code/controllers/subsystem/ticker.dm
+++ b/code/controllers/subsystem/ticker.dm
@@ -21,7 +21,11 @@ SUBSYSTEM_DEF(ticker)
var/list/login_music = null //Music played in pregame lobby
var/delay_end = FALSE //If set true, the round will not restart on it's own
+#if defined(UNIT_TESTS) //must be FALSE for unit tests else they hang indefinitely
var/delay_start = FALSE
+#else
+ var/delay_start = TRUE
+#endif
var/admin_delay_notice = "" //A message to display to anyone who tries to restart the world after a delay
var/time_left //Pre-game timer
From 966f427530221efc065538461bf37185c43410eb Mon Sep 17 00:00:00 2001
From: cm13-github <128137806+cm13-github@users.noreply.github.com>
Date: Tue, 30 Jul 2024 03:32:13 +0100
Subject: [PATCH 008/140] Automatic changelog for PR #351 [ci skip]
---
html/changelogs/AutoChangeLog-pr-351.yml | 4 ++++
1 file changed, 4 insertions(+)
create mode 100644 html/changelogs/AutoChangeLog-pr-351.yml
diff --git a/html/changelogs/AutoChangeLog-pr-351.yml b/html/changelogs/AutoChangeLog-pr-351.yml
new file mode 100644
index 0000000000..cabc118189
--- /dev/null
+++ b/html/changelogs/AutoChangeLog-pr-351.yml
@@ -0,0 +1,4 @@
+author: "Doubleumc"
+delete-after: True
+changes:
+ - admin: "round start delayed by default"
\ No newline at end of file
From 444fabdafad3342b1db10c4695d470ba4e94516a Mon Sep 17 00:00:00 2001
From: Changelogs
Date: Wed, 31 Jul 2024 01:05:21 +0000
Subject: [PATCH 009/140] Automatic changelog compile [ci skip]
---
html/changelogs/AutoChangeLog-pr-351.yml | 4 ----
html/changelogs/archive/2024-07.yml | 3 +++
2 files changed, 3 insertions(+), 4 deletions(-)
delete mode 100644 html/changelogs/AutoChangeLog-pr-351.yml
diff --git a/html/changelogs/AutoChangeLog-pr-351.yml b/html/changelogs/AutoChangeLog-pr-351.yml
deleted file mode 100644
index cabc118189..0000000000
--- a/html/changelogs/AutoChangeLog-pr-351.yml
+++ /dev/null
@@ -1,4 +0,0 @@
-author: "Doubleumc"
-delete-after: True
-changes:
- - admin: "round start delayed by default"
\ No newline at end of file
diff --git a/html/changelogs/archive/2024-07.yml b/html/changelogs/archive/2024-07.yml
index f171a09f14..f7d36bac1a 100644
--- a/html/changelogs/archive/2024-07.yml
+++ b/html/changelogs/archive/2024-07.yml
@@ -14,3 +14,6 @@
- code_imp: Made the Tech Tree subsystem initialise faster.
Zonespace27:
- admin: Added freed mob bans to the jobban panel.
+2024-07-31:
+ Doubleumc:
+ - admin: round start delayed by default
From 57b4b837d21b45a03edc3c6ade4a5180f08b670a Mon Sep 17 00:00:00 2001
From: Doubleumc
Date: Wed, 31 Jul 2024 10:48:07 -0400
Subject: [PATCH 010/140] Discord button size reduction (#352)
---
interface/skin.dmf | 10 +++-------
1 file changed, 3 insertions(+), 7 deletions(-)
diff --git a/interface/skin.dmf b/interface/skin.dmf
index 79ec957f08..ea5f18a91a 100644
--- a/interface/skin.dmf
+++ b/interface/skin.dmf
@@ -287,16 +287,12 @@ window "infowindow"
elem "discord"
type = BUTTON
pos = 420,5
- size = 210x45
+ size = 100x20
anchor1 = 66,0
- anchor2 = 97,0
+ anchor2 = 81,0
background-color = #7289da
- font-size = 20px
- font-family = "Arial"
- text-align = center
- font-weight = bold
saved-params = "is-checked"
- text = "discord.gg/PVE-CMSS13"
+ text = "Discord"
command = "discord"
window "outputwindow"
From e4db9795688f3ffea3046d58eacfb103e24f3757 Mon Sep 17 00:00:00 2001
From: cm13-github <128137806+cm13-github@users.noreply.github.com>
Date: Wed, 31 Jul 2024 15:49:21 +0100
Subject: [PATCH 011/140] Automatic changelog for PR #352 [ci skip]
---
html/changelogs/AutoChangeLog-pr-352.yml | 4 ++++
1 file changed, 4 insertions(+)
create mode 100644 html/changelogs/AutoChangeLog-pr-352.yml
diff --git a/html/changelogs/AutoChangeLog-pr-352.yml b/html/changelogs/AutoChangeLog-pr-352.yml
new file mode 100644
index 0000000000..069b98db96
--- /dev/null
+++ b/html/changelogs/AutoChangeLog-pr-352.yml
@@ -0,0 +1,4 @@
+author: "Doubleumc"
+delete-after: True
+changes:
+ - ui: "reduced size of Discord button"
\ No newline at end of file
From 6e5aef8cd218dba720733ee657dd2e23f7e0b664 Mon Sep 17 00:00:00 2001
From: Changelogs
Date: Thu, 1 Aug 2024 01:23:08 +0000
Subject: [PATCH 012/140] Automatic changelog compile [ci skip]
---
html/changelogs/AutoChangeLog-pr-352.yml | 4 ----
html/changelogs/archive/2024-08.yml | 3 +++
2 files changed, 3 insertions(+), 4 deletions(-)
delete mode 100644 html/changelogs/AutoChangeLog-pr-352.yml
create mode 100644 html/changelogs/archive/2024-08.yml
diff --git a/html/changelogs/AutoChangeLog-pr-352.yml b/html/changelogs/AutoChangeLog-pr-352.yml
deleted file mode 100644
index 069b98db96..0000000000
--- a/html/changelogs/AutoChangeLog-pr-352.yml
+++ /dev/null
@@ -1,4 +0,0 @@
-author: "Doubleumc"
-delete-after: True
-changes:
- - ui: "reduced size of Discord button"
\ No newline at end of file
diff --git a/html/changelogs/archive/2024-08.yml b/html/changelogs/archive/2024-08.yml
new file mode 100644
index 0000000000..f2770928ed
--- /dev/null
+++ b/html/changelogs/archive/2024-08.yml
@@ -0,0 +1,3 @@
+2024-08-01:
+ Doubleumc:
+ - ui: reduced size of Discord button
From 90ef5a2ddebcec209d6b49979680568bac73f151 Mon Sep 17 00:00:00 2001
From: Doubleumc
Date: Mon, 5 Aug 2024 01:43:52 -0400
Subject: [PATCH 013/140] Port TGUI react migration and fixes, Part 1 (#342)
Co-authored-by: Paul Mullen <101871009+mullenpaul@users.noreply.github.com>
Co-authored-by: SabreML <57483089+SabreML@users.noreply.github.com>
Co-authored-by: Drathek <76988376+Drulikar@users.noreply.github.com>
Co-authored-by: Jeremiah <42397676+jlsnow301@users.noreply.github.com>
Co-authored-by: Contrabang <91113370+Contrabang@users.noreply.github.com>
Co-authored-by: private-tristan <54422837+private-tristan@users.noreply.github.com>
Co-authored-by: harryob
Co-authored-by: fira
Co-authored-by: harryob <55142896+harryob@users.noreply.github.com>
Co-authored-by: Birdtalon
Co-authored-by: Jerm
Co-authored-by: Cthulhu80 <122310258+Cthulhu80@users.noreply.github.com>
Co-authored-by: ihatethisengine <115417687+ihatethisengine@users.noreply.github.com>
Co-authored-by: cuberound <122645057+cuberound@users.noreply.github.com>
Co-authored-by: vincibrv
Co-authored-by: Drulikar
Co-authored-by: Vero <73014819+vero5123@users.noreply.github.com>
Co-authored-by: Vicacrov <49321394+Vicacrov@users.noreply.github.com>
---
.prettierignore | 5 +
.vscode/settings.json | 3 +-
code/__DEFINES/__game.dm | 7 +-
code/__DEFINES/_math.dm | 2 +
.../dcs/signals/atom/signals_atom.dm | 3 +-
.../dcs/signals/atom/signals_item.dm | 10 +
.../__DEFINES/dcs/signals/atom/signals_obj.dm | 2 +
.../dcs/signals/atom/signals_turf.dm | 3 +
code/__DEFINES/dcs/signals/signals_global.dm | 2 +
code/__DEFINES/equipment.dm | 12 +-
code/__DEFINES/layers.dm | 1 +
code/__DEFINES/maps.dm | 7 +-
code/__DEFINES/mob_hud.dm | 1 +
code/__DEFINES/objects.dm | 5 +
code/__DEFINES/shuttles.dm | 2 +-
code/__DEFINES/tgui.dm | 6 +
code/__DEFINES/traits.dm | 2 +
code/__DEFINES/turf_flags.dm | 18 +-
code/__DEFINES/turfs.dm | 29 +
code/__DEFINES/typecheck/generic_types.dm | 1 +
code/__HELPERS/lighting.dm | 2 +
code/__HELPERS/lists.dm | 6 +-
code/__HELPERS/logging.dm | 10 +
code/__HELPERS/string_lists.dm | 23 +
code/__HELPERS/text.dm | 18 +
code/__HELPERS/unsorted.dm | 14 +-
code/_globalvars/bitfields.dm | 6 +
code/_globalvars/global_lists.dm | 3 +
code/_onclick/hud/map_popups.dm | 3 +-
code/_onclick/hud/rendering/plane_master.dm | 8 +
.../hud/rendering/plane_master_controller.dm | 4 +-
code/controllers/subsystem/atoms.dm | 14 +-
code/controllers/subsystem/interior.dm | 9 +-
code/controllers/subsystem/mapping.dm | 183 +-
code/controllers/subsystem/shuttles.dm | 489 +-
code/datums/ammo/bullet/pistol.dm | 4 +-
code/datums/ammo/bullet/revolver.dm | 2 +-
code/datums/ammo/bullet/rifle.dm | 2 +-
code/datums/ammo/bullet/smg.dm | 2 +-
code/datums/components/phone.dm | 35 +-
code/datums/entities/player.dm | 36 +
code/datums/mob_hud.dm | 10 +-
code/datums/shuttles.dm | 50 +-
code/datums/supply_packs/gear.dm | 29 +
code/game/atoms.dm | 14 +-
code/game/camera_manager/camera_manager.dm | 246 +
.../cas_manager/datums/cas_fire_envelope.dm | 248 +-
.../cas_manager/datums/cas_fire_mission.dm | 119 +-
.../game/machinery/computer/camera_console.dm | 106 +-
code/game/machinery/computer/demo_sim.dm | 4 +-
.../machinery/computer/dropship_weapons.dm | 1404 +-
code/game/machinery/doors/multi_tile.dm | 7 +-
.../vending/vendor_types/requisitions.dm | 2 +-
code/game/objects/items/fulton.dm | 10 +-
code/game/objects/items/misc.dm | 14 +-
.../structures/stool_bed_chair_nest/bed.dm | 9 +
code/game/sim_manager/datums/simulator.dm | 22 +-
code/game/turfs/closed.dm | 15 +
code/game/turfs/transit.dm | 238 +-
code/game/turfs/turf.dm | 51 +-
code/game/turfs/walls/wall_types.dm | 2 +-
code/game/turfs/walls/walls.dm | 3 +
code/game/world.dm | 34 +-
code/modules/admin/game_master/game_master.dm | 2 +-
code/modules/admin/holder2.dm | 16 +-
.../admin/player_panel/player_panel.dm | 2 +
code/modules/admin/verbs/debug.dm | 23 +-
code/modules/admin/verbs/load_event_level.dm | 5 +-
.../view_variables/color_matrix_editor.dm | 1 +
.../admin/view_variables/filterrific.dm | 4 +-
.../admin/view_variables/get_variables.dm | 4 +-
code/modules/asset_cache/assets/tgui.dm | 22 +
code/modules/client/client_procs.dm | 8 +-
code/modules/client/preferences.dm | 2 +-
code/modules/client/preferences_savefile.dm | 2 +-
code/modules/client/tgui_macro.dm | 2 +-
code/modules/clothing/suits/marine_armor.dm | 7 +-
code/modules/cm_marines/dropship_equipment.dm | 303 +-
code/modules/cm_phone/phone_base.dm | 8 +-
code/modules/droppod/droppod_ui.dm | 22 +
.../dropships/attach_points/attach_point.dm | 2 +-
.../dropships/cas/fire_mission_record.dm | 5 +
.../lighting_static/static_lighting_turf.dm | 5 +-
code/modules/logging/global_logs.dm | 3 +
code/modules/mapping/map_template.dm | 136 +-
code/modules/mapping/preloader.dm | 27 +-
code/modules/mapping/reader.dm | 1085 +-
.../mapping/space_management/space_level.dm | 15 +-
.../space_management/space_reservation.dm | 225 +-
.../space_management/zlevel_manager.dm | 12 +-
code/modules/mob/living/carbon/human/human.dm | 53 +-
.../living/carbon/human/human_attackhand.dm | 6 -
.../mob/living/carbon/human/human_damage.dm | 12 +-
.../mob/living/carbon/human/human_defines.dm | 2 +-
.../mob/living/carbon/human/update_icons.dm | 16 -
.../modules/mob/living/living_health_procs.dm | 2 +-
code/modules/mob/living/living_healthscan.dm | 19 +-
.../silicon/robot/drone/drone_damage.dm | 2 +-
.../mob/living/silicon/robot/robot_damage.dm | 2 +-
code/modules/mob/mob.dm | 2 +
code/modules/nightmare/nmtasks/mapload.dm | 23 +-
code/modules/power/apc.dm | 1 +
code/modules/shuttle/computer.dm | 4 +-
.../shuttle/computers/dropship_computer.dm | 2 +-
code/modules/shuttle/docking.dm | 2 -
code/modules/shuttle/shuttle.dm | 103 +-
code/modules/shuttle/shuttles/dropship.dm | 32 +-
.../shuttle/shuttles/trijent_elevator.dm | 8 +-
code/modules/tgui/tgui-say/modal.dm | 16 +-
code/modules/tgui/tgui-say/typing.dm | 1 +
code/modules/tgui/tgui.dm | 2 +-
code/modules/tgui/tgui_alert.dm | 91 +-
code/modules/tgui/tgui_input_list.dm | 147 +-
code/modules/tgui/tgui_number_input.dm | 81 +-
code/modules/tgui/tgui_window.dm | 22 +-
code/modules/tgui_input/checkboxes.dm | 211 +
code/modules/tgui_input/text.dm | 32 +-
code/modules/vehicles/interior/interior.dm | 16 +-
colonialmarines.dme | 4 +
dependencies.sh | 6 +-
icons/effects/Targeted.dmi | Bin 7493 -> 7272 bytes
icons/mob/hud/marine_hud.dmi | Bin 5849 -> 13035 bytes
icons/mob/humans/onmob/back.dmi | Bin 113615 -> 111822 bytes
icons/mob/humans/onmob/items_lefthand_0.dmi | Bin 135958 -> 131091 bytes
icons/mob/humans/onmob/items_righthand_0.dmi | Bin 133829 -> 128736 bytes
icons/obj/items/clothing/backpacks.dmi | Bin 60210 -> 61399 bytes
maps/map_files/USS_Almayer/USS_Almayer.dmm | 8 +-
.../derelict_almayer/derelict_almayer.dmm | 8 +-
maps/map_files/rover/rover.dmm | 2 +-
maps/shuttles/dropship_alamo.dmm | 57 +-
maps/shuttles/dropship_cyclone.dmm | 46 +-
maps/shuttles/dropship_midway.dmm | 45 +-
maps/shuttles/dropship_normandy.dmm | 46 +-
maps/shuttles/dropship_upp.dmm | 38 +-
nano/templates/dropship_weapons_console.tmpl | 245 -
node_modules/.yarn-integrity | 10 +
tgui/.eslintignore | 12 +-
tgui/.eslintrc-sonar.yml | 27 +-
tgui/.eslintrc.yml | 25 +-
tgui/.prettierignore | 1 +
tgui/.prettierrc.yml | 14 -
tgui/.swcrc | 15 +
.../@yarnpkg/plugin-interactive-tools.cjs | 363 -
tgui/.yarn/releases/yarn-3.1.1.cjs | 768 -
tgui/.yarn/releases/yarn-4.1.1.cjs | 893 ++
tgui/.yarn/sdks/eslint/bin/eslint.js | 4 +-
tgui/.yarn/sdks/eslint/lib/api.js | 4 +-
tgui/.yarn/sdks/eslint/lib/unsupported-api.js | 20 +
tgui/.yarn/sdks/eslint/package.json | 12 +-
tgui/.yarn/sdks/prettier/bin/prettier.cjs | 20 +
tgui/.yarn/sdks/prettier/index.cjs | 20 +
tgui/.yarn/sdks/prettier/index.js | 20 -
tgui/.yarn/sdks/prettier/package.json | 7 +-
tgui/.yarn/sdks/typescript/bin/tsc | 4 +-
tgui/.yarn/sdks/typescript/bin/tsserver | 4 +-
tgui/.yarn/sdks/typescript/lib/tsc.js | 4 +-
tgui/.yarn/sdks/typescript/lib/tsserver.js | 6 +-
.../sdks/typescript/lib/tsserverlibrary.js | 6 +-
tgui/.yarn/sdks/typescript/lib/typescript.js | 10 +-
tgui/.yarn/sdks/typescript/package.json | 8 +-
tgui/.yarnrc.yml | 12 +-
tgui/README.md | 8 +-
tgui/babel.config.js | 44 -
tgui/docs/component-reference.md | 265 +-
tgui/docs/converting-old-tgui-interfaces.md | 8 +-
tgui/docs/state-usage.md | 30 +
tgui/docs/tutorial-and-examples.md | 20 +-
tgui/global.d.ts | 32 +-
tgui/jest.config.js | 2 +-
tgui/package.json | 78 +-
tgui/packages/common/collections.ts | 282 +-
tgui/packages/common/color.js | 67 -
tgui/packages/common/color.test.ts | 49 +
tgui/packages/common/color.ts | 94 +
tgui/packages/common/events.test.ts | 34 +
tgui/packages/common/{events.js => events.ts} | 12 +-
tgui/packages/common/fp.js | 49 -
tgui/packages/common/fp.test.ts | 23 +
tgui/packages/common/fp.ts | 38 +
.../common/{keycodes.js => keycodes.ts} | 0
tgui/packages/common/keys.ts | 39 +
tgui/packages/common/package.json | 2 +-
tgui/packages/common/perf.js | 61 -
tgui/packages/common/perf.ts | 73 +
tgui/packages/common/ping.js | 108 +
tgui/packages/common/react.ts | 15 +-
tgui/packages/common/redux.js | 151 -
tgui/packages/common/redux.test.ts | 68 +
tgui/packages/common/redux.ts | 196 +
tgui/packages/common/string.babel-plugin.cjs | 73 -
tgui/packages/common/string.js | 196 -
tgui/packages/common/string.test.ts | 35 +
tgui/packages/common/string.ts | 174 +
tgui/packages/common/{timer.js => timer.ts} | 32 +-
tgui/packages/common/type-utils.ts | 42 +
tgui/packages/common/uuid.test.ts | 11 +
tgui/packages/common/{uuid.js => uuid.ts} | 15 +-
tgui/packages/common/vector.js | 48 -
tgui/packages/common/vector.ts | 51 +
tgui/packages/tgfont/package.json | 2 +-
tgui/packages/tgui-bench/entrypoint.tsx | 4 +-
tgui/packages/tgui-bench/index.js | 2 +-
tgui/packages/tgui-bench/lib/benchmark.d.ts | 2 +-
tgui/packages/tgui-bench/package.json | 9 +-
.../packages/tgui-bench/tests/Button.test.tsx | 23 -
.../tgui-bench/tests/Tooltip.test.tsx | 2 +-
tgui/packages/tgui-dev-server/dreamseeker.js | 3 +-
tgui/packages/tgui-dev-server/index.js | 2 +-
tgui/packages/tgui-dev-server/link/client.cjs | 64 +-
tgui/packages/tgui-dev-server/link/retrace.js | 3 +-
tgui/packages/tgui-dev-server/link/server.js | 1 +
tgui/packages/tgui-dev-server/package.json | 10 +-
tgui/packages/tgui-dev-server/reloader.js | 7 +-
tgui/packages/tgui-dev-server/util.js | 2 +
tgui/packages/tgui-dev-server/webpack.js | 1 +
tgui/packages/tgui-dev-server/winreg.js | 5 +-
.../{Notifications.js => Notifications.tsx} | 0
.../tgui-panel/{Panel.js => Panel.tsx} | 40 +-
.../tgui-panel/audio/NowPlayingWidget.js | 71 -
.../tgui-panel/audio/NowPlayingWidget.jsx | 111 +
.../tgui-panel/audio/{hooks.js => hooks.ts} | 9 +-
.../tgui-panel/audio/{index.js => index.ts} | 0
tgui/packages/tgui-panel/audio/player.js | 4 -
.../audio/{reducer.js => reducer.ts} | 0
.../audio/{selectors.js => selectors.ts} | 0
...atPageSettings.js => ChatPageSettings.jsx} | 60 +-
.../chat/{ChatPanel.js => ChatPanel.jsx} | 16 +-
.../chat/{ChatTabs.js => ChatTabs.jsx} | 38 +-
tgui/packages/tgui-panel/chat/actions.js | 2 +
.../chat/{constants.js => constants.ts} | 0
.../tgui-panel/chat/{index.js => index.ts} | 0
tgui/packages/tgui-panel/chat/middleware.js | 32 +-
tgui/packages/tgui-panel/chat/model.js | 4 +-
tgui/packages/tgui-panel/chat/reducer.js | 11 +-
.../chat/{renderer.js => renderer.jsx} | 155 +-
.../tgui-panel/chat/replaceInTextNode.js | 8 +-
.../chat/{selectors.js => selectors.ts} | 2 +-
.../game/{actions.js => actions.ts} | 0
.../game/{constants.js => constants.ts} | 2 +-
tgui/packages/tgui-panel/game/hooks.js | 12 -
tgui/packages/tgui-panel/game/hooks.ts | 13 +
.../tgui-panel/game/{index.js => index.ts} | 0
tgui/packages/tgui-panel/game/middleware.js | 2 +-
.../game/{reducer.js => reducer.ts} | 2 +-
.../game/{selectors.js => selectors.ts} | 0
.../tgui-panel/{index.js => index.tsx} | 51 +-
tgui/packages/tgui-panel/package.json | 9 +-
tgui/packages/tgui-panel/panelFocus.js | 2 +-
.../packages/tgui-panel/ping/PingIndicator.js | 27 -
.../tgui-panel/ping/PingIndicator.tsx | 39 +
.../ping/{actions.js => actions.ts} | 0
.../ping/{constants.js => constants.ts} | 0
.../tgui-panel/ping/{index.js => index.ts} | 0
.../ping/{reducer.js => reducer.ts} | 20 +-
.../ping/{selectors.js => selectors.ts} | 0
tgui/packages/tgui-panel/reconnect.tsx | 6 +-
.../{SettingsPanel.js => SettingsPanel.jsx} | 235 +-
.../settings/{actions.js => actions.ts} | 7 +-
.../settings/{constants.js => constants.ts} | 0
.../settings/{hooks.js => hooks.ts} | 11 +-
.../settings/{index.js => index.ts} | 0
.../tgui-panel/settings/middleware.js | 11 +-
.../settings/{model.js => model.ts} | 4 +-
tgui/packages/tgui-panel/settings/reducer.js | 15 +-
.../settings/{selectors.js => selectors.ts} | 0
.../tgui-panel/styles/goon/chat-dark.scss | 18 +
.../tgui-panel/styles/goon/chat-light.scss | 18 +
tgui/packages/tgui-panel/styles/main.scss | 2 +-
.../tgui-panel/styles/themes/light.scss | 2 +-
tgui/packages/tgui-panel/telemetry.js | 17 +-
.../tgui-panel/{themes.js => themes.ts} | 2 +-
tgui/packages/tgui-polyfill/00-html5shiv.js | 331 -
tgui/packages/tgui-polyfill/01-ie8.js | 837 --
tgui/packages/tgui-polyfill/02-dom4.js | 973 --
tgui/packages/tgui-polyfill/03-css-om.js | 45 -
tgui/packages/tgui-polyfill/1-misc.js | 48 +
tgui/packages/tgui-polyfill/10-misc.js | 62 -
tgui/packages/tgui-polyfill/package.json | 12 +-
.../packages/tgui-say/ChannelIterator.test.ts | 48 +
tgui/packages/tgui-say/ChannelIterator.ts | 71 +
tgui/packages/tgui-say/ChatHistory.test.ts | 50 +
tgui/packages/tgui-say/ChatHistory.ts | 59 +
tgui/packages/tgui-say/TguiSay.tsx | 370 +
.../packages/tgui-say/components/dragzone.tsx | 20 -
.../{constants/index.tsx => constants.ts} | 7 +-
.../packages/tgui-say/fonts/VT323-Regular.ttf | Bin 0 -> 147320 bytes
tgui/packages/tgui-say/handlers/arrowKeys.tsx | 22 -
.../tgui-say/handlers/backspaceDelete.tsx | 22 -
tgui/packages/tgui-say/handlers/click.tsx | 9 -
.../tgui-say/handlers/componentMount.tsx | 27 -
.../tgui-say/handlers/componentUpdate.tsx | 6 -
tgui/packages/tgui-say/handlers/enter.tsx | 23 -
tgui/packages/tgui-say/handlers/escape.tsx | 8 -
tgui/packages/tgui-say/handlers/force.tsx | 20 -
.../tgui-say/handlers/incrementChannel.tsx | 32 -
tgui/packages/tgui-say/handlers/index.tsx | 41 -
tgui/packages/tgui-say/handlers/input.tsx | 11 -
tgui/packages/tgui-say/handlers/keyDown.tsx | 43 -
.../tgui-say/handlers/radioPrefix.tsx | 34 -
tgui/packages/tgui-say/handlers/reset.tsx | 22 -
tgui/packages/tgui-say/handlers/setSize.tsx | 22 -
.../tgui-say/handlers/viewHistory.tsx | 25 -
tgui/packages/tgui-say/helpers.ts | 46 +
tgui/packages/tgui-say/helpers/index.tsx | 177 -
tgui/packages/tgui-say/index.tsx | 25 +-
tgui/packages/tgui-say/interfaces/TguiSay.tsx | 97 -
tgui/packages/tgui-say/package.json | 7 +-
tgui/packages/tgui-say/styles/button.scss | 17 +-
tgui/packages/tgui-say/styles/dragzone.scss | 76 +-
tgui/packages/tgui-say/styles/main.scss | 59 +-
tgui/packages/tgui-say/styles/modal.scss | 71 -
tgui/packages/tgui-say/styles/textarea.scss | 15 +-
tgui/packages/tgui-say/styles/window.scss | 36 +
tgui/packages/tgui-say/timers.ts | 19 +
tgui/packages/tgui-say/types/index.tsx | 59 -
tgui/packages/tgui/assets.js | 37 -
tgui/packages/tgui/assets.ts | 45 +
tgui/packages/tgui/assets/bg-cat.svg | 24 +
.../assets/bg-synthsunset-c-grid-size.svg | 49 +
.../tgui/assets/bg-synthsunset-c-grid.svg | 49 +
tgui/packages/tgui/backend.ts | 68 +-
.../tgui/components/AnimatedNumber.tsx | 11 +-
tgui/packages/tgui/components/Autofocus.tsx | 33 +-
.../tgui/components/{Blink.js => Blink.jsx} | 13 +-
.../{BlockQuote.js => BlockQuote.tsx} | 9 +-
.../tgui/components/BodyZoneSelector.tsx | 52 +-
tgui/packages/tgui/components/Box.tsx | 282 +-
tgui/packages/tgui/components/Button.js | 363 -
tgui/packages/tgui/components/Button.tsx | 430 +
.../components/{ByondUi.js => ByondUi.jsx} | 17 +-
tgui/packages/tgui/components/Chart.js | 127 -
tgui/packages/tgui/components/Chart.tsx | 160 +
tgui/packages/tgui/components/Collapsible.js | 45 -
tgui/packages/tgui/components/Collapsible.tsx | 46 +
tgui/packages/tgui/components/ColorBox.js | 31 -
tgui/packages/tgui/components/ColorBox.tsx | 31 +
tgui/packages/tgui/components/Dialog.tsx | 85 +
.../tgui/components/{Dimmer.js => Dimmer.tsx} | 11 +-
.../components/{Divider.js => Divider.tsx} | 13 +-
tgui/packages/tgui/components/DmIcon.tsx | 73 +
...aggableControl.js => DraggableControl.jsx} | 27 +-
tgui/packages/tgui/components/Dropdown.js | 161 -
tgui/packages/tgui/components/Dropdown.tsx | 247 +
tgui/packages/tgui/components/FitText.tsx | 32 +-
tgui/packages/tgui/components/Flex.tsx | 90 +-
tgui/packages/tgui/components/Grid.js | 38 -
tgui/packages/tgui/components/Icon.js | 71 -
tgui/packages/tgui/components/Icon.tsx | 91 +
tgui/packages/tgui/components/Image.tsx | 64 +
.../{InfinitePlane.js => InfinitePlane.jsx} | 44 +-
tgui/packages/tgui/components/Input.js | 156 -
tgui/packages/tgui/components/Input.tsx | 182 +
tgui/packages/tgui/components/KeyListener.tsx | 7 +-
.../tgui/components/{Knob.js => Knob.tsx} | 108 +-
...LabeledControls.js => LabeledControls.tsx} | 23 +-
tgui/packages/tgui/components/LabeledList.tsx | 107 +-
tgui/packages/tgui/components/MenuBar.tsx | 238 +
.../tgui/components/{Modal.js => Modal.tsx} | 12 +-
tgui/packages/tgui/components/NoticeBox.js | 27 -
tgui/packages/tgui/components/NoticeBox.tsx | 53 +
tgui/packages/tgui/components/NumberInput.js | 284 -
tgui/packages/tgui/components/NumberInput.tsx | 378 +
tgui/packages/tgui/components/Popper.tsx | 155 +-
.../{ProgressBar.js => ProgressBar.tsx} | 57 +-
.../tgui/components/RestrictedInput.js | 155 -
.../tgui/components/RestrictedInput.jsx | 301 +
.../{RoundGauge.js => RoundGauge.tsx} | 128 +-
tgui/packages/tgui/components/Section.tsx | 136 +-
.../tgui/components/{Slider.js => Slider.tsx} | 96 +-
tgui/packages/tgui/components/Stack.tsx | 66 +-
.../tgui/components/StyleableSection.tsx | 30 +
tgui/packages/tgui/components/Table.js | 64 -
tgui/packages/tgui/components/Table.tsx | 91 +
.../tgui/components/{Tabs.js => Tabs.tsx} | 39 +-
.../components/{TextArea.js => TextArea.jsx} | 19 +-
tgui/packages/tgui/components/TextArea.tsx | 198 +
.../{TimeDisplay.js => TimeDisplay.jsx} | 3 +-
tgui/packages/tgui/components/Tooltip.tsx | 73 +-
.../tgui/components/TrackOutsideClicks.tsx | 10 +-
tgui/packages/tgui/components/VirtualList.tsx | 69 +
.../tgui/components/{index.js => index.ts} | 13 +-
tgui/packages/tgui/constants.test.ts | 75 +
.../tgui/{constants.js => constants.ts} | 87 +-
.../debug/{KitchenSink.js => KitchenSink.jsx} | 18 +-
tgui/packages/tgui/debug/hooks.js | 4 +-
.../tgui/debug/{index.js => index.ts} | 0
tgui/packages/tgui/debug/middleware.js | 7 +-
tgui/packages/tgui/{drag.js => drag.ts} | 157 +-
tgui/packages/tgui/events.test.ts | 66 +
tgui/packages/tgui/{events.js => events.ts} | 69 +-
tgui/packages/tgui/{focus.js => focus.ts} | 0
tgui/packages/tgui/format.js | 197 -
tgui/packages/tgui/format.test.ts | 118 +
tgui/packages/tgui/format.ts | 173 +
tgui/packages/tgui/hotkeys.ts | 4 +
tgui/packages/tgui/http.ts | 2 +-
tgui/packages/tgui/{index.js => index.tsx} | 21 +-
.../interfaces/{AcidVest.js => AcidVest.jsx} | 26 +-
tgui/packages/tgui/interfaces/Adminhelp.tsx | 50 +-
tgui/packages/tgui/interfaces/AlertModal.tsx | 221 +-
.../{AlmayerControl.js => AlmayerControl.jsx} | 101 +-
...lConsole.js => AltitudeControlConsole.jsx} | 24 +-
.../{AntiAirConsole.js => AntiAirConsole.jsx} | 44 +-
.../tgui/interfaces/{Apc.js => Apc.jsx} | 83 +-
.../{AresInterface.js => AresInterface.jsx} | 547 +-
.../{Autodispenser.js => Autodispenser.jsx} | 83 +-
.../{Autolathe.js => Autolathe.jsx} | 94 +-
.../{Binoculars.js => Binoculars.jsx} | 9 +-
...eticPrinter.js => BioSyntheticPrinter.jsx} | 28 +-
.../{BotanyEditor.js => BotanyEditor.jsx} | 28 +-
...BotanyExtractor.js => BotanyExtractor.jsx} | 45 +-
.../interfaces/{BrigCell.js => BrigCell.jsx} | 46 +-
.../packages/tgui/interfaces/CameraConsole.js | 137 -
.../tgui/interfaces/CameraConsole.tsx | 221 +
.../{CanvasLayer.js => CanvasLayer.jsx} | 18 +-
.../interfaces/{CardMod.js => CardMod.jsx} | 109 +-
tgui/packages/tgui/interfaces/CasSim.js | 109 -
tgui/packages/tgui/interfaces/CasSim.tsx | 127 +
.../{Centrifuge.js => Centrifuge.jsx} | 27 +-
.../{Changelog.js => Changelog.jsx} | 129 +-
.../tgui/interfaces/CheckboxInput.tsx | 120 +
.../{ChemDispenser.js => ChemDispenser.jsx} | 44 +-
.../{ChooseFruit.js => ChooseFruit.jsx} | 25 +-
.../{ChooseResin.js => ChooseResin.jsx} | 25 +-
.../tgui/interfaces/ColorMatrixEditor.tsx | 28 +-
.../{CommandTablet.js => CommandTablet.jsx} | 50 +-
.../{CrewConsole.js => CrewConsole.jsx} | 25 +-
tgui/packages/tgui/interfaces/CrtPanel.tsx | 22 +
.../tgui/interfaces/{Cryo.js => Cryo.jsx} | 60 +-
.../interfaces/{DemoSim.js => DemoSim.tsx} | 76 +-
.../{Disposals.js => Disposals.jsx} | 25 +-
.../interfaces/{DrawnMap.js => DrawnMap.jsx} | 8 +-
.../tgui/interfaces/DropshipFlightControl.tsx | 140 +-
.../interfaces/DropshipWeaponsConsole.tsx | 385 +
.../tgui/interfaces/ElevatorControl.tsx | 31 +-
.../tgui/interfaces/EscapePodConsole.tsx | 21 +-
.../{FaxMachine.js => FaxMachine.jsx} | 89 +-
.../{Filteriffic.js => Filteriffic.jsx} | 120 +-
...rationControl.js => FiltrationControl.jsx} | 13 +-
.../{GameMaster.js => GameMaster.jsx} | 76 +-
...RappelMenu.js => GameMasterRappelMenu.jsx} | 23 +-
...uAmbush.js => GameMasterSubmenuAmbush.jsx} | 38 +-
...uInfest.js => GameMasterSubmenuInfest.jsx} | 21 +-
.../{HealthScan.js => HealthScan.jsx} | 100 +-
.../{HiveFaction.js => HiveFaction.jsx} | 18 +-
.../{HiveLeaders.js => HiveLeaders.jsx} | 24 +-
.../{HiveStatus.js => HiveStatus.jsx} | 121 +-
tgui/packages/tgui/interfaces/JoeEmotes.tsx | 43 +-
.../interfaces/{KeyBinds.js => KeyBinds.jsx} | 151 +-
.../{KillPanel.js => KillPanel.jsx} | 20 +-
.../{LanguageMenu.js => LanguageMenu.jsx} | 20 +-
tgui/packages/tgui/interfaces/ListInput.js | 234 -
.../tgui/interfaces/ListInputModal.tsx | 239 -
.../ListInputWindow/ListInputModal.tsx | 233 +
.../tgui/interfaces/ListInputWindow/index.tsx | 46 +
.../interfaces/LootPanel/GroupedContents.tsx | 48 +
.../tgui/interfaces/LootPanel/IconDisplay.tsx | 33 +
.../tgui/interfaces/LootPanel/LootBox.tsx | 61 +
.../tgui/interfaces/LootPanel/RawContents.tsx | 29 +
.../tgui/interfaces/LootPanel/index.tsx | 77 +
.../tgui/interfaces/LootPanel/types.ts | 13 +
tgui/packages/tgui/interfaces/MarkMenu.tsx | 116 +-
.../{MedalsPanel.js => MedalsPanel.jsx} | 64 +-
.../tgui/interfaces/MfdPanels/CameraPanel.tsx | 38 +
.../interfaces/MfdPanels/EquipmentPanel.tsx | 347 +
.../interfaces/MfdPanels/FiremissionPanel.tsx | 592 +
.../tgui/interfaces/MfdPanels/FultonPanel.tsx | 163 +
.../tgui/interfaces/MfdPanels/MGPanel.tsx | 73 +
.../tgui/interfaces/MfdPanels/MapPanel.tsx | 38 +
.../interfaces/MfdPanels/MedevacPanel.tsx | 204 +
.../MfdPanels/MultifunctionDisplay.tsx | 155 +
.../interfaces/MfdPanels/ParadropPanel.tsx | 77 +
.../tgui/interfaces/MfdPanels/SentryPanel.tsx | 91 +
.../interfaces/MfdPanels/SpotlightPanel.tsx | 64 +
.../interfaces/MfdPanels/SupportPanel.tsx | 70 +
.../interfaces/MfdPanels/TargetAquisition.tsx | 580 +
.../tgui/interfaces/MfdPanels/WeaponPanel.tsx | 205 +
.../interfaces/MfdPanels/stateManagers.ts | 95 +
.../tgui/interfaces/MfdPanels/types.ts | 118 +
tgui/packages/tgui/interfaces/Microwave.tsx | 21 +-
.../tgui/interfaces/{Mortar.js => Mortar.jsx} | 76 +-
.../tgui/interfaces/NavigationShuttle.tsx | 162 +-
.../{NuclearBomb.js => NuclearBomb.jsx} | 80 +-
.../tgui/interfaces/NumberInputModal.tsx | 52 +-
.../packages/tgui/interfaces/Orbit/helpers.ts | 25 +-
tgui/packages/tgui/interfaces/Orbit/index.tsx | 262 +-
...nonConsole.js => OrbitalCannonConsole.jsx} | 45 +-
...erwatchConsole.js => OverwatchConsole.jsx} | 196 +-
.../{PartFabricator.js => PartFabricator.jsx} | 60 +-
.../interfaces/ParticleEdit/EntriesBasic.tsx | 144 +-
.../ParticleEdit/EntriesGenerators.tsx | 106 +-
.../interfaces/ParticleEdit/Generators.tsx | 57 +-
.../tgui/interfaces/ParticleEdit/Tutorial.tsx | 146 +-
.../tgui/interfaces/ParticleEdit/data.ts | 24 +-
.../tgui/interfaces/ParticleEdit/helpers.ts | 6 +-
.../tgui/interfaces/ParticleEdit/index.tsx | 98 +-
.../{PhoneMenu.js => PhoneMenu.jsx} | 85 +-
.../tgui/interfaces/PingRelaysPanel.tsx | 206 +
.../{PlayerPanel.js => PlayerPanel.jsx} | 399 +-
tgui/packages/tgui/interfaces/Playtime.tsx | 25 +-
.../{PodLauncher.js => PodLauncher.jsx} | 350 +-
.../tgui/interfaces/PortableVendor.tsx | 14 +-
.../{Proximity.js => Proximity.jsx} | 22 +-
.../tgui/interfaces/PublishedDocsHud.tsx | 10 +-
tgui/packages/tgui/interfaces/Radar.tsx | 42 +-
.../tgui/interfaces/{Radio.js => Radio.jsx} | 24 +-
.../tgui/interfaces/ResearchDoorDisplay.js | 86 -
.../tgui/interfaces/ResearchDoorDisplay.jsx | 69 +
...searchMemories.js => ResearchMemories.jsx} | 31 +-
.../tgui/interfaces/ResearchTerminal.tsx | 299 +-
tgui/packages/tgui/interfaces/ResinPanel.tsx | 22 +-
tgui/packages/tgui/interfaces/STUI.js | 116 -
tgui/packages/tgui/interfaces/STUI.tsx | 143 +
...ructConsole.js => SelfDestructConsole.jsx} | 24 +-
.../{Sentencing.js => Sentencing.jsx} | 101 +-
tgui/packages/tgui/interfaces/SentryGunUI.tsx | 217 +-
.../tgui/interfaces/ShuttleManipulator.tsx | 228 +-
.../{Signaller.js => Signaller.jsx} | 12 +-
.../{SkillsMenu.js => SkillsMenu.jsx} | 30 +-
.../interfaces/{Sleeper.js => Sleeper.jsx} | 101 +-
tgui/packages/tgui/interfaces/SmartFridge.tsx | 97 +-
.../tgui/interfaces/{Smes.js => Smes.jsx} | 26 +-
tgui/packages/tgui/interfaces/SoundPanel.tsx | 138 +-
tgui/packages/tgui/interfaces/SquadInfo.tsx | 51 +-
.../interfaces/{SquadMod.js => SquadMod.jsx} | 20 +-
...owserOptions.js => StatbrowserOptions.jsx} | 26 +-
...lertConsole.js => StationAlertConsole.jsx} | 9 +-
...lyDropConsole.js => SupplyDropConsole.jsx} | 46 +-
...cmapAdminPanel.js => TacmapAdminPanel.jsx} | 45 +-
tgui/packages/tgui/interfaces/TacticalMap.tsx | 172 +-
.../tgui/interfaces/{Tank.js => Tank.jsx} | 23 +-
.../{TechControl.js => TechControl.jsx} | 26 +-
.../{TechMemories.js => TechMemories.tsx} | 75 +-
.../interfaces/{TechNode.js => TechNode.jsx} | 26 +-
...porterConsole.js => TeleporterConsole.jsx} | 32 +-
.../tgui/interfaces/TextInputModal.tsx | 46 +-
.../tgui/interfaces/{Timer.js => Timer.jsx} | 9 +-
.../{VehicleStatus.js => VehicleStatus.jsx} | 45 +-
tgui/packages/tgui/interfaces/Vending.tsx | 84 +-
.../tgui/interfaces/VendingSorted.tsx | 157 +-
.../interfaces/{VoteMenu.js => VoteMenu.jsx} | 55 +-
.../interfaces/{VoxPanel.js => VoxPanel.jsx} | 124 +-
.../packages/tgui/interfaces/VultureScope.tsx | 89 +-
.../{WeaponStats.js => WeaponStats.jsx} | 135 +-
.../tgui/interfaces/{Wires.js => Wires.jsx} | 29 +-
.../{WorkingJoe.js => WorkingJoe.jsx} | 271 +-
.../packages/tgui/interfaces/YautjaEmotes.tsx | 43 +-
.../common/{AccessList.js => AccessList.jsx} | 62 +-
.../{BeakerContents.js => BeakerContents.jsx} | 0
tgui/packages/tgui/interfaces/common/Dpad.tsx | 115 +
.../interfaces/common/ElectricalPanel.tsx | 69 +-
.../tgui/interfaces/common/InputButtons.tsx | 49 +-
...oticeBox.js => InterfaceLockNoticeBox.jsx} | 11 +-
.../tgui/interfaces/common/Loader.tsx | 3 +-
.../tgui/interfaces/common/LoadingToolbox.tsx | 32 +
.../tgui/interfaces/common/TimedCallback.tsx | 6 +-
tgui/packages/tgui/layouts/Layout.js | 45 -
tgui/packages/tgui/layouts/Layout.tsx | 76 +
.../layouts/{NtosWindow.js => NtosWindow.tsx} | 94 +-
.../tgui/layouts/{Pane.js => Pane.tsx} | 35 +-
tgui/packages/tgui/layouts/Window.js | 202 -
tgui/packages/tgui/layouts/Window.tsx | 234 +
.../tgui/layouts/{index.js => index.ts} | 0
tgui/packages/tgui/links.test.ts | 79 +
tgui/packages/tgui/{links.js => links.ts} | 14 +-
tgui/packages/tgui/logging.js | 59 -
tgui/packages/tgui/logging.ts | 68 +
tgui/packages/tgui/package.json | 21 +-
tgui/packages/tgui/renderer.ts | 34 +-
tgui/packages/tgui/routes.js | 101 -
tgui/packages/tgui/routes.tsx | 99 +
tgui/packages/tgui/sanitize.test.ts | 36 +
.../tgui/{sanitize.js => sanitize.ts} | 27 +-
tgui/packages/tgui/store.js | 95 -
tgui/packages/tgui/store.ts | 111 +
.../{Blink.stories.js => Blink.stories.jsx} | 2 +-
...uote.stories.js => BlockQuote.stories.jsx} | 2 +-
.../{Box.stories.js => Box.stories.jsx} | 2 +-
.../{Button.stories.js => Button.stories.jsx} | 34 +-
...ByondUi.stories.js => ByondUi.stories.jsx} | 22 +-
...ble.stories.js => Collapsible.stories.jsx} | 2 +-
.../{Flex.stories.js => Flex.stories.jsx} | 26 +-
.../{Input.stories.js => Input.stories.jsx} | 28 +-
...ist.stories.js => LabeledList.stories.jsx} | 8 +-
.../{Popper.stories.js => Popper.stories.tsx} | 22 +-
...Bar.stories.js => ProgressBar.stories.jsx} | 40 +-
.../{Stack.stories.js => Stack.stories.jsx} | 4 +-
...Storage.stories.js => Storage.stories.jsx} | 9 +-
.../{Tabs.stories.js => Tabs.stories.jsx} | 64 +-
.../{Themes.stories.js => Themes.stories.jsx} | 10 +-
...Tooltip.stories.js => Tooltip.stories.jsx} | 7 +-
.../tgui/stories/{common.js => common.jsx} | 0
.../tgui/styles/components/Button.scss | 27 +-
.../tgui/styles/components/Dialog.scss | 105 +
.../tgui/styles/components/Dropdown.scss | 44 +-
.../tgui/styles/components/MenuBar.scss | 75 +
.../tgui/styles/components/ProgressBar.scss | 4 +-
.../tgui/styles/components/SearchItem.scss | 35 +
.../tgui/styles/components/Stack.scss | 24 +
.../tgui/styles/components/TextArea.scss | 6 +
.../tgui/styles/components/Tooltip.scss | 2 +-
.../tgui/styles/interfaces/AlertModal.scss | 4 +-
.../tgui/styles/interfaces/CasSim.scss | 17 +
.../tgui/styles/interfaces/CrtPanel.scss | 74 +
.../styles/interfaces/DropshipWeapons.scss | 253 +
.../tgui/styles/interfaces/ListInput.scss | 4 +-
.../tgui/styles/interfaces/SidePanel.scss | 21 +
.../tgui/styles/interfaces/SmartFridge.scss | 18 +-
.../tgui/styles/interfaces/SquadInfo.scss | 4 +-
.../tgui/styles/interfaces/TacticalMap.scss | 8 +
.../tgui/styles/interfaces/VendingSorted.scss | 29 +-
.../tgui/styles/interfaces/common/Dpad.scss | 26 +
.../interfaces/common/ElectricalPanel.scss | 12 +-
.../tgui/styles/layouts/SplitWindow.scss | 13 +
.../tgui/styles/layouts/TitleBar.scss | 8 +-
tgui/packages/tgui/styles/main.scss | 9 +
.../packages/tgui/styles/themes/abductor.scss | 4 +-
tgui/packages/tgui/styles/themes/admin.scss | 23 +-
.../tgui/styles/themes/cardtable.scss | 4 +-
tgui/packages/tgui/styles/themes/crt.scss | 30 +-
.../tgui/styles/themes/hackerman.scss | 4 +-
.../tgui/styles/themes/hive_status.scss | 4 +-
.../tgui/styles/themes/malfunction.scss | 4 +-
tgui/packages/tgui/styles/themes/neutral.scss | 14 +-
tgui/packages/tgui/styles/themes/ntOS95.scss | 166 +
tgui/packages/tgui/styles/themes/ntos.scss | 4 +-
.../packages/tgui/styles/themes/ntos_cat.scss | 148 +
.../tgui/styles/themes/ntos_darkmode.scss | 44 +
.../tgui/styles/themes/ntos_lightmode.scss | 67 +
.../tgui/styles/themes/ntos_spooky.scss | 69 +
.../tgui/styles/themes/ntos_synth.scss | 99 +
.../tgui/styles/themes/ntos_terminal.scss | 112 +
tgui/packages/tgui/styles/themes/paper.scss | 4 +-
tgui/packages/tgui/styles/themes/retro.scss | 6 +-
.../tgui/styles/themes/spookyconsole.scss | 4 +-
.../tgui/styles/themes/syndicate.scss | 4 +-
tgui/packages/tgui/styles/themes/uscm.scss | 9 +-
tgui/packages/tgui/styles/themes/weyland.scss | 4 +-
tgui/packages/tgui/styles/themes/wizard.scss | 16 +-
tgui/packages/tgui/styles/themes/xeno.scss | 16 +-
tgui/public/tgui-panel.bundle.css | 2 -
tgui/public/tgui-polyfill.min.js | 2 +-
tgui/public/tgui.html | 62 +-
tgui/tsconfig.json | 9 +-
tgui/webpack.config.js | 43 +-
tgui/yarn.lock | 11737 +++++++---------
tools/bootstrap/node | 4 +-
tools/bootstrap/node_.ps1 | 3 +-
tools/build/build.js | 2 +-
tools/build/juke/index.js | 2 +-
650 files changed, 31708 insertions(+), 22673 deletions(-)
create mode 100644 .prettierignore
create mode 100644 code/__DEFINES/turfs.dm
create mode 100644 code/__HELPERS/string_lists.dm
create mode 100644 code/game/camera_manager/camera_manager.dm
create mode 100644 code/modules/tgui_input/checkboxes.dm
delete mode 100644 nano/templates/dropship_weapons_console.tmpl
create mode 100644 node_modules/.yarn-integrity
create mode 100644 tgui/.swcrc
delete mode 100644 tgui/.yarn/plugins/@yarnpkg/plugin-interactive-tools.cjs
delete mode 100644 tgui/.yarn/releases/yarn-3.1.1.cjs
create mode 100644 tgui/.yarn/releases/yarn-4.1.1.cjs
create mode 100644 tgui/.yarn/sdks/eslint/lib/unsupported-api.js
create mode 100644 tgui/.yarn/sdks/prettier/bin/prettier.cjs
create mode 100644 tgui/.yarn/sdks/prettier/index.cjs
delete mode 100644 tgui/.yarn/sdks/prettier/index.js
delete mode 100644 tgui/babel.config.js
create mode 100644 tgui/docs/state-usage.md
delete mode 100644 tgui/packages/common/color.js
create mode 100644 tgui/packages/common/color.test.ts
create mode 100644 tgui/packages/common/color.ts
create mode 100644 tgui/packages/common/events.test.ts
rename tgui/packages/common/{events.js => events.ts} (76%)
delete mode 100644 tgui/packages/common/fp.js
create mode 100644 tgui/packages/common/fp.test.ts
create mode 100644 tgui/packages/common/fp.ts
rename tgui/packages/common/{keycodes.js => keycodes.ts} (100%)
create mode 100644 tgui/packages/common/keys.ts
delete mode 100644 tgui/packages/common/perf.js
create mode 100644 tgui/packages/common/perf.ts
create mode 100644 tgui/packages/common/ping.js
delete mode 100644 tgui/packages/common/redux.js
create mode 100644 tgui/packages/common/redux.test.ts
create mode 100644 tgui/packages/common/redux.ts
delete mode 100644 tgui/packages/common/string.babel-plugin.cjs
delete mode 100644 tgui/packages/common/string.js
create mode 100644 tgui/packages/common/string.test.ts
create mode 100644 tgui/packages/common/string.ts
rename tgui/packages/common/{timer.js => timer.ts} (59%)
create mode 100644 tgui/packages/common/type-utils.ts
create mode 100644 tgui/packages/common/uuid.test.ts
rename tgui/packages/common/{uuid.js => uuid.ts} (57%)
delete mode 100644 tgui/packages/common/vector.js
create mode 100644 tgui/packages/common/vector.ts
rename tgui/packages/tgui-panel/{Notifications.js => Notifications.tsx} (100%)
rename tgui/packages/tgui-panel/{Panel.js => Panel.tsx} (76%)
delete mode 100644 tgui/packages/tgui-panel/audio/NowPlayingWidget.js
create mode 100644 tgui/packages/tgui-panel/audio/NowPlayingWidget.jsx
rename tgui/packages/tgui-panel/audio/{hooks.js => hooks.ts} (51%)
rename tgui/packages/tgui-panel/audio/{index.js => index.ts} (100%)
rename tgui/packages/tgui-panel/audio/{reducer.js => reducer.ts} (100%)
rename tgui/packages/tgui-panel/audio/{selectors.js => selectors.ts} (100%)
rename tgui/packages/tgui-panel/chat/{ChatPageSettings.js => ChatPageSettings.jsx} (63%)
rename tgui/packages/tgui-panel/chat/{ChatPanel.js => ChatPanel.jsx} (84%)
rename tgui/packages/tgui-panel/chat/{ChatTabs.js => ChatTabs.jsx} (65%)
rename tgui/packages/tgui-panel/chat/{constants.js => constants.ts} (100%)
rename tgui/packages/tgui-panel/chat/{index.js => index.ts} (100%)
rename tgui/packages/tgui-panel/chat/{renderer.js => renderer.jsx} (84%)
rename tgui/packages/tgui-panel/chat/{selectors.js => selectors.ts} (85%)
rename tgui/packages/tgui-panel/game/{actions.js => actions.ts} (100%)
rename tgui/packages/tgui-panel/game/{constants.js => constants.ts} (61%)
delete mode 100644 tgui/packages/tgui-panel/game/hooks.js
create mode 100644 tgui/packages/tgui-panel/game/hooks.ts
rename tgui/packages/tgui-panel/game/{index.js => index.ts} (100%)
rename tgui/packages/tgui-panel/game/{reducer.js => reducer.ts} (94%)
rename tgui/packages/tgui-panel/game/{selectors.js => selectors.ts} (100%)
rename tgui/packages/tgui-panel/{index.js => index.tsx} (81%)
delete mode 100644 tgui/packages/tgui-panel/ping/PingIndicator.js
create mode 100644 tgui/packages/tgui-panel/ping/PingIndicator.tsx
rename tgui/packages/tgui-panel/ping/{actions.js => actions.ts} (100%)
rename tgui/packages/tgui-panel/ping/{constants.js => constants.ts} (100%)
rename tgui/packages/tgui-panel/ping/{index.js => index.ts} (100%)
rename tgui/packages/tgui-panel/ping/{reducer.js => reducer.ts} (71%)
rename tgui/packages/tgui-panel/ping/{selectors.js => selectors.ts} (100%)
rename tgui/packages/tgui-panel/settings/{SettingsPanel.js => SettingsPanel.jsx} (57%)
rename tgui/packages/tgui-panel/settings/{actions.js => actions.ts} (90%)
rename tgui/packages/tgui-panel/settings/{constants.js => constants.ts} (100%)
rename tgui/packages/tgui-panel/settings/{hooks.js => hooks.ts} (52%)
rename tgui/packages/tgui-panel/settings/{index.js => index.ts} (100%)
rename tgui/packages/tgui-panel/settings/{model.js => model.ts} (65%)
rename tgui/packages/tgui-panel/settings/{selectors.js => selectors.ts} (100%)
rename tgui/packages/tgui-panel/{themes.js => themes.ts} (99%)
delete mode 100644 tgui/packages/tgui-polyfill/00-html5shiv.js
delete mode 100644 tgui/packages/tgui-polyfill/01-ie8.js
delete mode 100644 tgui/packages/tgui-polyfill/02-dom4.js
delete mode 100644 tgui/packages/tgui-polyfill/03-css-om.js
create mode 100644 tgui/packages/tgui-polyfill/1-misc.js
delete mode 100644 tgui/packages/tgui-polyfill/10-misc.js
create mode 100644 tgui/packages/tgui-say/ChannelIterator.test.ts
create mode 100644 tgui/packages/tgui-say/ChannelIterator.ts
create mode 100644 tgui/packages/tgui-say/ChatHistory.test.ts
create mode 100644 tgui/packages/tgui-say/ChatHistory.ts
create mode 100644 tgui/packages/tgui-say/TguiSay.tsx
delete mode 100644 tgui/packages/tgui-say/components/dragzone.tsx
rename tgui/packages/tgui-say/{constants/index.tsx => constants.ts} (98%)
create mode 100644 tgui/packages/tgui-say/fonts/VT323-Regular.ttf
delete mode 100644 tgui/packages/tgui-say/handlers/arrowKeys.tsx
delete mode 100644 tgui/packages/tgui-say/handlers/backspaceDelete.tsx
delete mode 100644 tgui/packages/tgui-say/handlers/click.tsx
delete mode 100644 tgui/packages/tgui-say/handlers/componentMount.tsx
delete mode 100644 tgui/packages/tgui-say/handlers/componentUpdate.tsx
delete mode 100644 tgui/packages/tgui-say/handlers/enter.tsx
delete mode 100644 tgui/packages/tgui-say/handlers/escape.tsx
delete mode 100644 tgui/packages/tgui-say/handlers/force.tsx
delete mode 100644 tgui/packages/tgui-say/handlers/incrementChannel.tsx
delete mode 100644 tgui/packages/tgui-say/handlers/index.tsx
delete mode 100644 tgui/packages/tgui-say/handlers/input.tsx
delete mode 100644 tgui/packages/tgui-say/handlers/keyDown.tsx
delete mode 100644 tgui/packages/tgui-say/handlers/radioPrefix.tsx
delete mode 100644 tgui/packages/tgui-say/handlers/reset.tsx
delete mode 100644 tgui/packages/tgui-say/handlers/setSize.tsx
delete mode 100644 tgui/packages/tgui-say/handlers/viewHistory.tsx
create mode 100644 tgui/packages/tgui-say/helpers.ts
delete mode 100644 tgui/packages/tgui-say/helpers/index.tsx
delete mode 100644 tgui/packages/tgui-say/interfaces/TguiSay.tsx
delete mode 100644 tgui/packages/tgui-say/styles/modal.scss
create mode 100644 tgui/packages/tgui-say/styles/window.scss
create mode 100644 tgui/packages/tgui-say/timers.ts
delete mode 100644 tgui/packages/tgui-say/types/index.tsx
delete mode 100644 tgui/packages/tgui/assets.js
create mode 100644 tgui/packages/tgui/assets.ts
create mode 100644 tgui/packages/tgui/assets/bg-cat.svg
create mode 100644 tgui/packages/tgui/assets/bg-synthsunset-c-grid-size.svg
create mode 100644 tgui/packages/tgui/assets/bg-synthsunset-c-grid.svg
rename tgui/packages/tgui/components/{Blink.js => Blink.jsx} (89%)
rename tgui/packages/tgui/components/{BlockQuote.js => BlockQuote.tsx} (65%)
delete mode 100644 tgui/packages/tgui/components/Button.js
create mode 100644 tgui/packages/tgui/components/Button.tsx
rename tgui/packages/tgui/components/{ByondUi.js => ByondUi.jsx} (88%)
delete mode 100644 tgui/packages/tgui/components/Chart.js
create mode 100644 tgui/packages/tgui/components/Chart.tsx
delete mode 100644 tgui/packages/tgui/components/Collapsible.js
create mode 100644 tgui/packages/tgui/components/Collapsible.tsx
delete mode 100644 tgui/packages/tgui/components/ColorBox.js
create mode 100644 tgui/packages/tgui/components/ColorBox.tsx
create mode 100644 tgui/packages/tgui/components/Dialog.tsx
rename tgui/packages/tgui/components/{Dimmer.js => Dimmer.tsx} (57%)
rename tgui/packages/tgui/components/{Divider.js => Divider.tsx} (62%)
create mode 100644 tgui/packages/tgui/components/DmIcon.tsx
rename tgui/packages/tgui/components/{DraggableControl.js => DraggableControl.jsx} (93%)
delete mode 100644 tgui/packages/tgui/components/Dropdown.js
create mode 100644 tgui/packages/tgui/components/Dropdown.tsx
delete mode 100644 tgui/packages/tgui/components/Grid.js
delete mode 100644 tgui/packages/tgui/components/Icon.js
create mode 100644 tgui/packages/tgui/components/Icon.tsx
create mode 100644 tgui/packages/tgui/components/Image.tsx
rename tgui/packages/tgui/components/{InfinitePlane.js => InfinitePlane.jsx} (86%)
delete mode 100644 tgui/packages/tgui/components/Input.js
create mode 100644 tgui/packages/tgui/components/Input.tsx
rename tgui/packages/tgui/components/{Knob.js => Knob.tsx} (53%)
rename tgui/packages/tgui/components/{LabeledControls.js => LabeledControls.tsx} (70%)
create mode 100644 tgui/packages/tgui/components/MenuBar.tsx
rename tgui/packages/tgui/components/{Modal.js => Modal.tsx} (65%)
delete mode 100644 tgui/packages/tgui/components/NoticeBox.js
create mode 100644 tgui/packages/tgui/components/NoticeBox.tsx
delete mode 100644 tgui/packages/tgui/components/NumberInput.js
create mode 100644 tgui/packages/tgui/components/NumberInput.tsx
rename tgui/packages/tgui/components/{ProgressBar.js => ProgressBar.tsx} (50%)
delete mode 100644 tgui/packages/tgui/components/RestrictedInput.js
create mode 100644 tgui/packages/tgui/components/RestrictedInput.jsx
rename tgui/packages/tgui/components/{RoundGauge.js => RoundGauge.tsx} (51%)
rename tgui/packages/tgui/components/{Slider.js => Slider.tsx} (53%)
create mode 100644 tgui/packages/tgui/components/StyleableSection.tsx
delete mode 100644 tgui/packages/tgui/components/Table.js
create mode 100644 tgui/packages/tgui/components/Table.tsx
rename tgui/packages/tgui/components/{Tabs.js => Tabs.tsx} (63%)
rename tgui/packages/tgui/components/{TextArea.js => TextArea.jsx} (95%)
create mode 100644 tgui/packages/tgui/components/TextArea.tsx
rename tgui/packages/tgui/components/{TimeDisplay.js => TimeDisplay.jsx} (97%)
create mode 100644 tgui/packages/tgui/components/VirtualList.tsx
rename tgui/packages/tgui/components/{index.js => index.ts} (87%)
create mode 100644 tgui/packages/tgui/constants.test.ts
rename tgui/packages/tgui/{constants.js => constants.ts} (81%)
rename tgui/packages/tgui/debug/{KitchenSink.js => KitchenSink.jsx} (72%)
rename tgui/packages/tgui/debug/{index.js => index.ts} (100%)
rename tgui/packages/tgui/{drag.js => drag.ts} (63%)
create mode 100644 tgui/packages/tgui/events.test.ts
rename tgui/packages/tgui/{events.js => events.ts} (73%)
rename tgui/packages/tgui/{focus.js => focus.ts} (100%)
delete mode 100644 tgui/packages/tgui/format.js
create mode 100644 tgui/packages/tgui/format.test.ts
create mode 100644 tgui/packages/tgui/format.ts
rename tgui/packages/tgui/{index.js => index.tsx} (83%)
rename tgui/packages/tgui/interfaces/{AcidVest.js => AcidVest.jsx} (89%)
rename tgui/packages/tgui/interfaces/{AlmayerControl.js => AlmayerControl.jsx} (81%)
rename tgui/packages/tgui/interfaces/{AltitudeControlConsole.js => AltitudeControlConsole.jsx} (78%)
rename tgui/packages/tgui/interfaces/{AntiAirConsole.js => AntiAirConsole.jsx} (77%)
rename tgui/packages/tgui/interfaces/{Apc.js => Apc.jsx} (77%)
rename tgui/packages/tgui/interfaces/{AresInterface.js => AresInterface.jsx} (77%)
rename tgui/packages/tgui/interfaces/{Autodispenser.js => Autodispenser.jsx} (86%)
rename tgui/packages/tgui/interfaces/{Autolathe.js => Autolathe.jsx} (79%)
rename tgui/packages/tgui/interfaces/{Binoculars.js => Binoculars.jsx} (75%)
rename tgui/packages/tgui/interfaces/{BioSyntheticPrinter.js => BioSyntheticPrinter.jsx} (83%)
rename tgui/packages/tgui/interfaces/{BotanyEditor.js => BotanyEditor.jsx} (81%)
rename tgui/packages/tgui/interfaces/{BotanyExtractor.js => BotanyExtractor.jsx} (78%)
rename tgui/packages/tgui/interfaces/{BrigCell.js => BrigCell.jsx} (91%)
delete mode 100644 tgui/packages/tgui/interfaces/CameraConsole.js
create mode 100644 tgui/packages/tgui/interfaces/CameraConsole.tsx
rename tgui/packages/tgui/interfaces/{CanvasLayer.js => CanvasLayer.jsx} (96%)
rename tgui/packages/tgui/interfaces/{CardMod.js => CardMod.jsx} (78%)
delete mode 100644 tgui/packages/tgui/interfaces/CasSim.js
create mode 100644 tgui/packages/tgui/interfaces/CasSim.tsx
rename tgui/packages/tgui/interfaces/{Centrifuge.js => Centrifuge.jsx} (77%)
rename tgui/packages/tgui/interfaces/{Changelog.js => Changelog.jsx} (74%)
create mode 100644 tgui/packages/tgui/interfaces/CheckboxInput.tsx
rename tgui/packages/tgui/interfaces/{ChemDispenser.js => ChemDispenser.jsx} (84%)
rename tgui/packages/tgui/interfaces/{ChooseFruit.js => ChooseFruit.jsx} (81%)
rename tgui/packages/tgui/interfaces/{ChooseResin.js => ChooseResin.jsx} (83%)
rename tgui/packages/tgui/interfaces/{CommandTablet.js => CommandTablet.jsx} (82%)
rename tgui/packages/tgui/interfaces/{CrewConsole.js => CrewConsole.jsx} (92%)
create mode 100644 tgui/packages/tgui/interfaces/CrtPanel.tsx
rename tgui/packages/tgui/interfaces/{Cryo.js => Cryo.jsx} (72%)
rename tgui/packages/tgui/interfaces/{DemoSim.js => DemoSim.tsx} (65%)
rename tgui/packages/tgui/interfaces/{Disposals.js => Disposals.jsx} (60%)
rename tgui/packages/tgui/interfaces/{DrawnMap.js => DrawnMap.jsx} (96%)
create mode 100644 tgui/packages/tgui/interfaces/DropshipWeaponsConsole.tsx
rename tgui/packages/tgui/interfaces/{FaxMachine.js => FaxMachine.jsx} (63%)
rename tgui/packages/tgui/interfaces/{Filteriffic.js => Filteriffic.jsx} (78%)
rename tgui/packages/tgui/interfaces/{FiltrationControl.js => FiltrationControl.jsx} (73%)
rename tgui/packages/tgui/interfaces/{GameMaster.js => GameMaster.jsx} (83%)
rename tgui/packages/tgui/interfaces/{GameMasterRappelMenu.js => GameMasterRappelMenu.jsx} (80%)
rename tgui/packages/tgui/interfaces/{GameMasterSubmenuAmbush.js => GameMasterSubmenuAmbush.jsx} (82%)
rename tgui/packages/tgui/interfaces/{GameMasterSubmenuInfest.js => GameMasterSubmenuInfest.jsx} (86%)
rename tgui/packages/tgui/interfaces/{HealthScan.js => HealthScan.jsx} (85%)
rename tgui/packages/tgui/interfaces/{HiveFaction.js => HiveFaction.jsx} (79%)
rename tgui/packages/tgui/interfaces/{HiveLeaders.js => HiveLeaders.jsx} (84%)
rename tgui/packages/tgui/interfaces/{HiveStatus.js => HiveStatus.jsx} (86%)
rename tgui/packages/tgui/interfaces/{KeyBinds.js => KeyBinds.jsx} (71%)
rename tgui/packages/tgui/interfaces/{KillPanel.js => KillPanel.jsx} (77%)
rename tgui/packages/tgui/interfaces/{LanguageMenu.js => LanguageMenu.jsx} (70%)
delete mode 100644 tgui/packages/tgui/interfaces/ListInput.js
delete mode 100644 tgui/packages/tgui/interfaces/ListInputModal.tsx
create mode 100644 tgui/packages/tgui/interfaces/ListInputWindow/ListInputModal.tsx
create mode 100644 tgui/packages/tgui/interfaces/ListInputWindow/index.tsx
create mode 100644 tgui/packages/tgui/interfaces/LootPanel/GroupedContents.tsx
create mode 100644 tgui/packages/tgui/interfaces/LootPanel/IconDisplay.tsx
create mode 100644 tgui/packages/tgui/interfaces/LootPanel/LootBox.tsx
create mode 100644 tgui/packages/tgui/interfaces/LootPanel/RawContents.tsx
create mode 100644 tgui/packages/tgui/interfaces/LootPanel/index.tsx
create mode 100644 tgui/packages/tgui/interfaces/LootPanel/types.ts
rename tgui/packages/tgui/interfaces/{MedalsPanel.js => MedalsPanel.jsx} (75%)
create mode 100644 tgui/packages/tgui/interfaces/MfdPanels/CameraPanel.tsx
create mode 100644 tgui/packages/tgui/interfaces/MfdPanels/EquipmentPanel.tsx
create mode 100644 tgui/packages/tgui/interfaces/MfdPanels/FiremissionPanel.tsx
create mode 100644 tgui/packages/tgui/interfaces/MfdPanels/FultonPanel.tsx
create mode 100644 tgui/packages/tgui/interfaces/MfdPanels/MGPanel.tsx
create mode 100644 tgui/packages/tgui/interfaces/MfdPanels/MapPanel.tsx
create mode 100644 tgui/packages/tgui/interfaces/MfdPanels/MedevacPanel.tsx
create mode 100644 tgui/packages/tgui/interfaces/MfdPanels/MultifunctionDisplay.tsx
create mode 100644 tgui/packages/tgui/interfaces/MfdPanels/ParadropPanel.tsx
create mode 100644 tgui/packages/tgui/interfaces/MfdPanels/SentryPanel.tsx
create mode 100644 tgui/packages/tgui/interfaces/MfdPanels/SpotlightPanel.tsx
create mode 100644 tgui/packages/tgui/interfaces/MfdPanels/SupportPanel.tsx
create mode 100644 tgui/packages/tgui/interfaces/MfdPanels/TargetAquisition.tsx
create mode 100644 tgui/packages/tgui/interfaces/MfdPanels/WeaponPanel.tsx
create mode 100644 tgui/packages/tgui/interfaces/MfdPanels/stateManagers.ts
create mode 100644 tgui/packages/tgui/interfaces/MfdPanels/types.ts
rename tgui/packages/tgui/interfaces/{Mortar.js => Mortar.jsx} (63%)
rename tgui/packages/tgui/interfaces/{NuclearBomb.js => NuclearBomb.jsx} (71%)
rename tgui/packages/tgui/interfaces/{OrbitalCannonConsole.js => OrbitalCannonConsole.jsx} (86%)
rename tgui/packages/tgui/interfaces/{OverwatchConsole.js => OverwatchConsole.jsx} (85%)
rename tgui/packages/tgui/interfaces/{PartFabricator.js => PartFabricator.jsx} (50%)
rename tgui/packages/tgui/interfaces/{PhoneMenu.js => PhoneMenu.jsx} (72%)
create mode 100644 tgui/packages/tgui/interfaces/PingRelaysPanel.tsx
rename tgui/packages/tgui/interfaces/{PlayerPanel.js => PlayerPanel.jsx} (78%)
rename tgui/packages/tgui/interfaces/{PodLauncher.js => PodLauncher.jsx} (76%)
rename tgui/packages/tgui/interfaces/{Proximity.js => Proximity.jsx} (81%)
rename tgui/packages/tgui/interfaces/{Radio.js => Radio.jsx} (88%)
delete mode 100644 tgui/packages/tgui/interfaces/ResearchDoorDisplay.js
create mode 100644 tgui/packages/tgui/interfaces/ResearchDoorDisplay.jsx
rename tgui/packages/tgui/interfaces/{ResearchMemories.js => ResearchMemories.jsx} (78%)
delete mode 100644 tgui/packages/tgui/interfaces/STUI.js
create mode 100644 tgui/packages/tgui/interfaces/STUI.tsx
rename tgui/packages/tgui/interfaces/{SelfDestructConsole.js => SelfDestructConsole.jsx} (81%)
rename tgui/packages/tgui/interfaces/{Sentencing.js => Sentencing.jsx} (85%)
rename tgui/packages/tgui/interfaces/{Signaller.js => Signaller.jsx} (87%)
rename tgui/packages/tgui/interfaces/{SkillsMenu.js => SkillsMenu.jsx} (80%)
rename tgui/packages/tgui/interfaces/{Sleeper.js => Sleeper.jsx} (83%)
rename tgui/packages/tgui/interfaces/{Smes.js => Smes.jsx} (94%)
rename tgui/packages/tgui/interfaces/{SquadMod.js => SquadMod.jsx} (78%)
rename tgui/packages/tgui/interfaces/{StatbrowserOptions.js => StatbrowserOptions.jsx} (70%)
rename tgui/packages/tgui/interfaces/{StationAlertConsole.js => StationAlertConsole.jsx} (89%)
rename tgui/packages/tgui/interfaces/{SupplyDropConsole.js => SupplyDropConsole.jsx} (79%)
rename tgui/packages/tgui/interfaces/{TacmapAdminPanel.js => TacmapAdminPanel.jsx} (86%)
rename tgui/packages/tgui/interfaces/{Tank.js => Tank.jsx} (89%)
rename tgui/packages/tgui/interfaces/{TechControl.js => TechControl.jsx} (88%)
rename tgui/packages/tgui/interfaces/{TechMemories.js => TechMemories.tsx} (66%)
rename tgui/packages/tgui/interfaces/{TechNode.js => TechNode.jsx} (81%)
rename tgui/packages/tgui/interfaces/{TeleporterConsole.js => TeleporterConsole.jsx} (85%)
rename tgui/packages/tgui/interfaces/{Timer.js => Timer.jsx} (84%)
rename tgui/packages/tgui/interfaces/{VehicleStatus.js => VehicleStatus.jsx} (82%)
rename tgui/packages/tgui/interfaces/{VoteMenu.js => VoteMenu.jsx} (82%)
rename tgui/packages/tgui/interfaces/{VoxPanel.js => VoxPanel.jsx} (77%)
rename tgui/packages/tgui/interfaces/{WeaponStats.js => WeaponStats.jsx} (77%)
rename tgui/packages/tgui/interfaces/{Wires.js => Wires.jsx} (77%)
rename tgui/packages/tgui/interfaces/{WorkingJoe.js => WorkingJoe.jsx} (83%)
rename tgui/packages/tgui/interfaces/common/{AccessList.js => AccessList.jsx} (73%)
rename tgui/packages/tgui/interfaces/common/{BeakerContents.js => BeakerContents.jsx} (100%)
create mode 100644 tgui/packages/tgui/interfaces/common/Dpad.tsx
rename tgui/packages/tgui/interfaces/common/{InterfaceLockNoticeBox.js => InterfaceLockNoticeBox.jsx} (84%)
create mode 100644 tgui/packages/tgui/interfaces/common/LoadingToolbox.tsx
delete mode 100644 tgui/packages/tgui/layouts/Layout.js
create mode 100644 tgui/packages/tgui/layouts/Layout.tsx
rename tgui/packages/tgui/layouts/{NtosWindow.js => NtosWindow.tsx} (60%)
rename tgui/packages/tgui/layouts/{Pane.js => Pane.tsx} (63%)
delete mode 100644 tgui/packages/tgui/layouts/Window.js
create mode 100644 tgui/packages/tgui/layouts/Window.tsx
rename tgui/packages/tgui/layouts/{index.js => index.ts} (100%)
create mode 100644 tgui/packages/tgui/links.test.ts
rename tgui/packages/tgui/{links.js => links.ts} (76%)
delete mode 100644 tgui/packages/tgui/logging.js
create mode 100644 tgui/packages/tgui/logging.ts
delete mode 100644 tgui/packages/tgui/routes.js
create mode 100644 tgui/packages/tgui/routes.tsx
create mode 100644 tgui/packages/tgui/sanitize.test.ts
rename tgui/packages/tgui/{sanitize.js => sanitize.ts} (53%)
delete mode 100644 tgui/packages/tgui/store.js
create mode 100644 tgui/packages/tgui/store.ts
rename tgui/packages/tgui/stories/{Blink.stories.js => Blink.stories.jsx} (88%)
rename tgui/packages/tgui/stories/{BlockQuote.stories.js => BlockQuote.stories.jsx} (90%)
rename tgui/packages/tgui/stories/{Box.stories.js => Box.stories.jsx} (93%)
rename tgui/packages/tgui/stories/{Button.stories.js => Button.stories.jsx} (56%)
rename tgui/packages/tgui/stories/{ByondUi.stories.js => ByondUi.stories.jsx} (81%)
rename tgui/packages/tgui/stories/{Collapsible.stories.js => Collapsible.stories.jsx} (92%)
rename tgui/packages/tgui/stories/{Flex.stories.js => Flex.stories.jsx} (75%)
rename tgui/packages/tgui/stories/{Input.stories.js => Input.stories.jsx} (84%)
rename tgui/packages/tgui/stories/{LabeledList.stories.js => LabeledList.stories.jsx} (92%)
rename tgui/packages/tgui/stories/{Popper.stories.js => Popper.stories.tsx} (81%)
rename tgui/packages/tgui/stories/{ProgressBar.stories.js => ProgressBar.stories.jsx} (52%)
rename tgui/packages/tgui/stories/{Stack.stories.js => Stack.stories.jsx} (93%)
rename tgui/packages/tgui/stories/{Storage.stories.js => Storage.stories.jsx} (93%)
rename tgui/packages/tgui/stories/{Tabs.stories.js => Tabs.stories.jsx} (70%)
rename tgui/packages/tgui/stories/{Themes.stories.js => Themes.stories.jsx} (61%)
rename tgui/packages/tgui/stories/{Tooltip.stories.js => Tooltip.stories.jsx} (87%)
rename tgui/packages/tgui/stories/{common.js => common.jsx} (100%)
create mode 100644 tgui/packages/tgui/styles/components/Dialog.scss
create mode 100644 tgui/packages/tgui/styles/components/MenuBar.scss
create mode 100644 tgui/packages/tgui/styles/components/SearchItem.scss
create mode 100644 tgui/packages/tgui/styles/interfaces/CasSim.scss
create mode 100644 tgui/packages/tgui/styles/interfaces/CrtPanel.scss
create mode 100644 tgui/packages/tgui/styles/interfaces/DropshipWeapons.scss
create mode 100644 tgui/packages/tgui/styles/interfaces/SidePanel.scss
create mode 100644 tgui/packages/tgui/styles/interfaces/common/Dpad.scss
create mode 100644 tgui/packages/tgui/styles/layouts/SplitWindow.scss
create mode 100644 tgui/packages/tgui/styles/themes/ntOS95.scss
create mode 100644 tgui/packages/tgui/styles/themes/ntos_cat.scss
create mode 100644 tgui/packages/tgui/styles/themes/ntos_darkmode.scss
create mode 100644 tgui/packages/tgui/styles/themes/ntos_lightmode.scss
create mode 100644 tgui/packages/tgui/styles/themes/ntos_spooky.scss
create mode 100644 tgui/packages/tgui/styles/themes/ntos_synth.scss
create mode 100644 tgui/packages/tgui/styles/themes/ntos_terminal.scss
delete mode 100644 tgui/public/tgui-panel.bundle.css
diff --git a/.prettierignore b/.prettierignore
new file mode 100644
index 0000000000..2b7500b231
--- /dev/null
+++ b/.prettierignore
@@ -0,0 +1,5 @@
+# We don't want prettier to run on anything outside of the TGUI folder, so we have to do this.
+/*
+
+# We want it to run into the TGUI folder, however.
+!/tgui
diff --git a/.vscode/settings.json b/.vscode/settings.json
index c7b218b775..d29a55ea06 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -1,7 +1,7 @@
{
"eslint.nodePath": "./tgui/.yarn/sdks",
"eslint.workingDirectories": ["./tgui"],
- "prettier.prettierPath": "./tgui/.yarn/sdks/prettier/index.js",
+ "prettier.prettierPath": "./tgui/.yarn/sdks/prettier/index.cjs",
"typescript.tsdk": "./tgui/.yarn/sdks/typescript/lib",
"typescript.enablePromptUseWorkspaceTsdk": true,
"search.exclude": {
@@ -14,6 +14,7 @@
"files.eol": "\n",
"files.insertFinalNewline": true,
"gitlens.advanced.blame.customArguments": ["-w"],
+ "tgstationTestExplorer.project.resultsType": "json",
"[javascript]": {
"editor.rulers": [80],
"editor.defaultFormatter": "esbenp.prettier-vscode",
diff --git a/code/__DEFINES/__game.dm b/code/__DEFINES/__game.dm
index 142ec4f887..5dcc85921a 100644
--- a/code/__DEFINES/__game.dm
+++ b/code/__DEFINES/__game.dm
@@ -1,9 +1,3 @@
-#define RANGE_TURFS(RADIUS, CENTER) \
-block( \
- locate(max(CENTER.x-(RADIUS),1), max(CENTER.y-(RADIUS),1), CENTER.z), \
- locate(min(CENTER.x+(RADIUS),world.maxx), min(CENTER.y+(RADIUS),world.maxy), CENTER.z) \
-)
-
//Admin perms are in global.dm.
/// To make it even more clear that something is a bitfield.
@@ -396,6 +390,7 @@ block( \
#define FIRE_MISSION_WEAPON_REMOVED 8
#define FIRE_MISSION_WEAPON_UNUSABLE 16
#define FIRE_MISSION_WEAPON_OUT_OF_AMMO 32
+#define FIRE_MISSION_BAD_DIRECTION 64
#define FIRE_MISSION_NOT_EXECUTABLE -1
//Defines for firemission state
diff --git a/code/__DEFINES/_math.dm b/code/__DEFINES/_math.dm
index a63fae4300..e16f769c3c 100644
--- a/code/__DEFINES/_math.dm
+++ b/code/__DEFINES/_math.dm
@@ -32,6 +32,8 @@
#define CEILING(x, y) ( -round(-(x) / (y)) * (y) )
+#define ROUND_UP(x) ( -round(-(x)))
+
// round() acts like floor(x, 1) by default but can't handle other values
#define FLOOR(x, y) ( round((x) / (y)) * (y) )
diff --git a/code/__DEFINES/dcs/signals/atom/signals_atom.dm b/code/__DEFINES/dcs/signals/atom/signals_atom.dm
index 394e63bd6e..2e75d85e38 100644
--- a/code/__DEFINES/dcs/signals/atom/signals_atom.dm
+++ b/code/__DEFINES/dcs/signals/atom/signals_atom.dm
@@ -1,6 +1,7 @@
/// From /atom/proc/Decorate
#define COMSIG_ATOM_DECORATED "atom_decorated"
-
+//from SSatoms InitAtom - Only if the atom was not deleted or failed initialization and has a loc
+#define COMSIG_ATOM_AFTER_SUCCESSFUL_INITIALIZED_ON "atom_init_success_on"
///from base of atom/setDir(): (old_dir, new_dir). Called before the direction changes.
#define COMSIG_ATOM_DIR_CHANGE "atom_dir_change"
diff --git a/code/__DEFINES/dcs/signals/atom/signals_item.dm b/code/__DEFINES/dcs/signals/atom/signals_item.dm
index 532437e4a0..d24cf55976 100644
--- a/code/__DEFINES/dcs/signals/atom/signals_item.dm
+++ b/code/__DEFINES/dcs/signals/atom/signals_item.dm
@@ -41,3 +41,13 @@
//Additional procs on items that will be triggered right after the human finishes spawns in
#define COMSIG_POST_SPAWN_UPDATE "post_spawn_update"
+
+#define COMSIG_CAMERA_MAPNAME_ASSIGNED "camera_manager_mapname_assigned"
+#define COMSIG_CAMERA_REGISTER_UI "camera_manager_register_ui"
+#define COMSIG_CAMERA_UNREGISTER_UI "camera_manager_unregister_ui"
+#define COMSIG_CAMERA_SET_NVG "camera_manager_set_nvg"
+#define COMSIG_CAMERA_CLEAR_NVG "camera_manager_clear_nvg"
+#define COMSIG_CAMERA_SET_TARGET "camera_manager_set_target"
+#define COMSIG_CAMERA_SET_AREA "camera_manager_set_area"
+#define COMSIG_CAMERA_CLEAR "camera_manager_clear_target"
+#define COMSIG_CAMERA_REFRESH "camera_manager_refresh"
diff --git a/code/__DEFINES/dcs/signals/atom/signals_obj.dm b/code/__DEFINES/dcs/signals/atom/signals_obj.dm
index 90aa4bd984..7ca874bc34 100644
--- a/code/__DEFINES/dcs/signals/atom/signals_obj.dm
+++ b/code/__DEFINES/dcs/signals/atom/signals_obj.dm
@@ -27,4 +27,6 @@
/// from /obj/proc/afterbuckle()
#define COSMIG_OBJ_AFTER_BUCKLE "signal_obj_after_buckle"
+#define COMSIG_DROPSHIP_ADD_EQUIPMENT "dropship_add_equipment"
+#define COMSIG_DROPSHIP_REMOVE_EQUIPMENT "dropship_remove_equipment"
#define COMSIG_STRUCTURE_CRATE_SQUAD_LAUNCHED "structure_crate_squad_launched"
diff --git a/code/__DEFINES/dcs/signals/atom/signals_turf.dm b/code/__DEFINES/dcs/signals/atom/signals_turf.dm
index b59e9d60b4..475aa9fdf4 100644
--- a/code/__DEFINES/dcs/signals/atom/signals_turf.dm
+++ b/code/__DEFINES/dcs/signals/atom/signals_turf.dm
@@ -19,3 +19,6 @@
///from /turf/closed/wall/proc/place_poster
#define COMSIG_POSTER_PLACED "poster_placed"
+
+///from base of /datum/turf_reservation/proc/Release: (datum/turf_reservation/reservation)
+#define COMSIG_TURF_RESERVATION_RELEASED "turf_reservation_released"
diff --git a/code/__DEFINES/dcs/signals/signals_global.dm b/code/__DEFINES/dcs/signals/signals_global.dm
index c452176f91..89904c5a1b 100644
--- a/code/__DEFINES/dcs/signals/signals_global.dm
+++ b/code/__DEFINES/dcs/signals/signals_global.dm
@@ -8,6 +8,8 @@
///from base of datum/controller/subsystem/mapping/proc/add_new_zlevel(): (list/args)
#define COMSIG_GLOB_NEW_Z "!new_z"
+/// sent after world.maxx and/or world.maxy are expanded: (has_exapnded_world_maxx, has_expanded_world_maxy)
+#define COMSIG_GLOB_EXPANDED_WORLD_BOUNDS "!expanded_world_bounds"
///from base of datum/controller/subsystem/mapping/proc/add_new_zlevel(): (list/args)
#define COMSIG_GLOB_VEHICLE_ORDERED "!vehicle_ordered"
/// from /datum/controller/subsystem/ticker/fire
diff --git a/code/__DEFINES/equipment.dm b/code/__DEFINES/equipment.dm
index 77638ed568..10894c4cdb 100644
--- a/code/__DEFINES/equipment.dm
+++ b/code/__DEFINES/equipment.dm
@@ -82,6 +82,10 @@
#define ANIMATED_SURGICAL_TOOL (1<<12)
/// Has heat source but isn't 'on fire' and thus can be stored
#define IGNITING_ITEM (1<<13)
+/// Overrides NODROP in some cases (stripping)
+#define FORCEDROP_CONDITIONAL (1<<14)
+/// Overrides smartgunner not being able to wear backpacks
+#define SMARTGUNNER_BACKPACK_OVERRIDE (1<<15)
//==========================================================================================
@@ -550,10 +554,10 @@ var/global/list/uniform_categories = list(
#define PHONE_UPP_SOLDIER "Soldier"
#define PHONE_IO "IO"
-#define PHONE_DO_NOT_DISTURB_FORCED 2
-#define PHONE_DO_NOT_DISTURB_ON 1
-#define PHONE_DO_NOT_DISTURB_OFF 0
-#define PHONE_DO_NOT_DISTURB_FORBIDDEN -1
+#define PHONE_DND_FORCED 2
+#define PHONE_DND_ON 1
+#define PHONE_DND_OFF 0
+#define PHONE_DND_FORBIDDEN -1
#define PHONE_ON_BASE_UNIT_ICON_STATE "[initial(icon_state)]"
#define PHONE_OFF_BASE_UNIT_ICON_STATE "[initial(icon_state)]_ear"
diff --git a/code/__DEFINES/layers.dm b/code/__DEFINES/layers.dm
index 598cfcdfec..31b1dd810d 100644
--- a/code/__DEFINES/layers.dm
+++ b/code/__DEFINES/layers.dm
@@ -233,6 +233,7 @@
///--------------- FULLSCREEN RUNECHAT BUBBLES ------------
#define LIGHTING_PLANE 100
#define EXTERIOR_LIGHTING_PLANE 101
+#define NVG_PLANE 110
///Popup Chat Messages
#define RUNECHAT_PLANE 501
diff --git a/code/__DEFINES/maps.dm b/code/__DEFINES/maps.dm
index 3f6a4a44ee..a3fdbe2f29 100644
--- a/code/__DEFINES/maps.dm
+++ b/code/__DEFINES/maps.dm
@@ -117,8 +117,5 @@ require only minor tweaks.
#define MAP_ARMOR_STYLE_JUNGLE "jungle"
#define MAP_ARMOR_STYLE_PRISON "prison"
-//turf-only flags
-#define NOJAUNT_1 (1<<0)
-#define UNUSED_RESERVATION_TURF (1<<1)
-/// If a turf can be made dirty at roundstart. This is also used in areas.
-#define CAN_BE_DIRTY_1 (1<<2)
+/// A map key that corresponds to being one exclusively for Space.
+#define SPACE_KEY "space"
diff --git a/code/__DEFINES/mob_hud.dm b/code/__DEFINES/mob_hud.dm
index cd1ad4af7c..271aeaebff 100644
--- a/code/__DEFINES/mob_hud.dm
+++ b/code/__DEFINES/mob_hud.dm
@@ -25,6 +25,7 @@
#define STATUS_HUD_XENO_CULTIST "24" // Whether they are a xeno cultist or not
#define HUNTER_CLAN "25" //Displays a colored icon to represent ingame Hunter Clans
#define HUNTER_HUD "26" //Displays various statuses on mobs for Hunters to identify targets
+#define HOLOCARD_HUD "27" //Displays the holocards set by medical personnel
//data HUD (medhud, sechud) defines
#define MOB_HUD_SECURITY_BASIC 1
diff --git a/code/__DEFINES/objects.dm b/code/__DEFINES/objects.dm
index a6b95c879a..5160e334ff 100644
--- a/code/__DEFINES/objects.dm
+++ b/code/__DEFINES/objects.dm
@@ -173,3 +173,8 @@ var/list/RESTRICTED_CAMERA_NETWORKS = list( //Those networks can only be accesse
#define CHECKS_PASSED 1
#define STILL_ON_COOLDOWN 2
#define NO_LIGHT_STATE_CHANGE 3
+
+//tool capabilities or something i don't know
+#define REMOVE_CROWBAR (1<<0)
+#define BREAK_CROWBAR (1<<1)
+#define REMOVE_SCREWDRIVER (1<<2)
diff --git a/code/__DEFINES/shuttles.dm b/code/__DEFINES/shuttles.dm
index 3356782946..a27c7a5689 100644
--- a/code/__DEFINES/shuttles.dm
+++ b/code/__DEFINES/shuttles.dm
@@ -41,7 +41,7 @@
#define TRANSIT_REQUEST 1
#define TRANSIT_READY 2
-#define SHUTTLE_TRANSIT_BORDER 8
+#define SHUTTLE_TRANSIT_BORDER 16
#define PARALLAX_LOOP_TIME 25
#define HYPERSPACE_END_TIME 5
diff --git a/code/__DEFINES/tgui.dm b/code/__DEFINES/tgui.dm
index ca6408961e..cf04ef686b 100644
--- a/code/__DEFINES/tgui.dm
+++ b/code/__DEFINES/tgui.dm
@@ -37,6 +37,12 @@
"%7b%22type%22%3a%22[type]%22%2c%22payload%22%3a[url_encode(json_encode(payload))]%7d" \
)
+/// Creates a message packet for sending via output() specifically for opening tgsay using an embedded winget
+// This is {"type":"open","payload":{"channel":channel,"mapfocus":[[map.focus]]}}, but pre-encoded.
+#define TGUI_CREATE_OPEN_MESSAGE(channel) ( \
+ "%7b%22type%22%3a%22open%22%2c%22payload%22%3a%7B%22channel%22%3a%22[channel]%22%2c%22mapfocus%22%3a\[\[map.focus\]\]%7d%7d" \
+)
+
/*
*Defines for the TGUI health analyser interface
*The higher the level, the more information you can see
diff --git a/code/__DEFINES/traits.dm b/code/__DEFINES/traits.dm
index 8962230946..3112d604a3 100644
--- a/code/__DEFINES/traits.dm
+++ b/code/__DEFINES/traits.dm
@@ -449,6 +449,8 @@ GLOBAL_LIST(trait_name_map)
#define XENO_WEED_TRAIT "xeno_weed"
/// traits associated with actively interacted machinery
#define INTERACTION_TRAIT "interaction"
+/// traits associated with interacting with a dropship
+#define TRAIT_SOURCE_DROPSHIP_INTERACTION "dropship_interaction"
/// traits bound by stunned status effects
#define STUNNED_TRAIT "stunned"
/// traits bound by knocked_down status effect
diff --git a/code/__DEFINES/turf_flags.dm b/code/__DEFINES/turf_flags.dm
index d7b3e90811..19dc17191d 100644
--- a/code/__DEFINES/turf_flags.dm
+++ b/code/__DEFINES/turf_flags.dm
@@ -1,3 +1,12 @@
+//turf_flags values
+/// Marks a turf as organic. Used for alien wall and membranes.
+#define TURF_ORGANIC (1<<0)
+/// If a turf is an usused reservation turf awaiting assignment
+#define UNUSED_RESERVATION_TURF (1<<1)
+/// If a turf is a reserved turf
+#define RESERVATION_TURF (1<<2)
+
+//ChangeTurf options to change its behavior
#define CHANGETURF_DEFER_CHANGE (1<<0)
/// This flag prevents changeturf from gathering air from nearby turfs to fill the new turf with an approximation of local air
#define CHANGETURF_IGNORE_AIR (1<<1)
@@ -5,12 +14,3 @@
/// A flag for PlaceOnTop to just instance the new turf instead of calling ChangeTurf. Used for uninitialized turfs NOTHING ELSE
#define CHANGETURF_SKIP (1<<3)
-#define IS_OPAQUE_TURF(turf) (turf.directional_opacity == ALL_CARDINALS)
-
-/// Marks a turf as organic. Used for alien wall and membranes.
-#define TURF_ORGANIC (1<<0)
-
-
-#define REMOVE_CROWBAR (1<<0)
-#define BREAK_CROWBAR (1<<1)
-#define REMOVE_SCREWDRIVER (1<<2)
diff --git a/code/__DEFINES/turfs.dm b/code/__DEFINES/turfs.dm
new file mode 100644
index 0000000000..b9a80d4ab2
--- /dev/null
+++ b/code/__DEFINES/turfs.dm
@@ -0,0 +1,29 @@
+#define RANGE_TURFS(RADIUS, CENTER) \
+block( \
+ locate(max(CENTER.x-(RADIUS),1), max(CENTER.y-(RADIUS),1), CENTER.z), \
+ locate(min(CENTER.x+(RADIUS),world.maxx), min(CENTER.y+(RADIUS),world.maxy), CENTER.z) \
+)
+
+#define RECT_TURFS(H_RADIUS, V_RADIUS, CENTER) \
+ block( \
+ locate(max((CENTER).x-(H_RADIUS),1), max((CENTER).y-(V_RADIUS),1), (CENTER).z), \
+ locate(min((CENTER).x+(H_RADIUS),world.maxx), min((CENTER).y+(V_RADIUS),world.maxy), (CENTER).z) \
+ )
+
+///Returns all turfs in a zlevel
+#define Z_TURFS(ZLEVEL) block(locate(1,1,ZLEVEL), locate(world.maxx, world.maxy, ZLEVEL))
+
+/// Returns a list of turfs in the rectangle specified by BOTTOM LEFT corner and height/width, checks for being outside the world border for you
+#define CORNER_BLOCK(corner, width, height) CORNER_BLOCK_OFFSET(corner, width, height, 0, 0)
+
+/// Returns a list of turfs similar to CORNER_BLOCK but with offsets
+#define CORNER_BLOCK_OFFSET(corner, width, height, offset_x, offset_y) ((block(locate(corner.x + offset_x, corner.y + offset_y, corner.z), locate(min(corner.x + (width - 1) + offset_x, world.maxx), min(corner.y + (height - 1) + offset_y, world.maxy), corner.z))))
+
+/// Returns an outline (neighboring turfs) of the given block
+#define CORNER_OUTLINE(corner, width, height) ( \
+ CORNER_BLOCK_OFFSET(corner, width + 2, 1, -1, -1) + \
+ CORNER_BLOCK_OFFSET(corner, width + 2, 1, -1, height) + \
+ CORNER_BLOCK_OFFSET(corner, 1, height, -1, 0) + \
+ CORNER_BLOCK_OFFSET(corner, 1, height, width, 0))
+
+#define TURF_FROM_COORDS_LIST(List) (locate(List[1], List[2], List[3]))
diff --git a/code/__DEFINES/typecheck/generic_types.dm b/code/__DEFINES/typecheck/generic_types.dm
index d9fa3df554..587108d5b5 100644
--- a/code/__DEFINES/typecheck/generic_types.dm
+++ b/code/__DEFINES/typecheck/generic_types.dm
@@ -11,6 +11,7 @@
#define ismovableatom(A) (ismovable(A))
#define isatom(A) (isloc(A))
#define isfloorturf(A) (istype(A, /turf/open/floor))
+#define isclosedturf(A) (istype(A, /turf/closed))
#define isweakref(D) (istype(D, /datum/weakref))
#define isgenerator(A) (istype(A, /generator))
diff --git a/code/__HELPERS/lighting.dm b/code/__HELPERS/lighting.dm
index 08c360849b..e768d9d125 100644
--- a/code/__HELPERS/lighting.dm
+++ b/code/__HELPERS/lighting.dm
@@ -1,3 +1,5 @@
+#define IS_OPAQUE_TURF(turf) (turf.directional_opacity == ALL_CARDINALS)
+
/// Produces a mutable appearance glued to the [EMISSIVE_PLANE] dyed to be the [EMISSIVE_COLOR].
/proc/emissive_appearance(icon, icon_state = "", layer = FLOAT_LAYER, alpha = 255, appearance_flags = NONE)
var/mutable_appearance/appearance = mutable_appearance(icon, icon_state, layer, EMISSIVE_PLANE, alpha, appearance_flags | EMISSIVE_APPEARANCE_FLAGS)
diff --git a/code/__HELPERS/lists.dm b/code/__HELPERS/lists.dm
index 830e612712..2fc4fee95c 100644
--- a/code/__HELPERS/lists.dm
+++ b/code/__HELPERS/lists.dm
@@ -521,7 +521,7 @@
//Copies a list, and all lists inside it recusively
//Does not copy any other reference type
-/proc/deepCopyList(list/L)
+/proc/deep_copy_list(list/L)
if(!islist(L))
return L
. = L.Copy()
@@ -532,10 +532,10 @@
continue
var/value = .[key]
if(islist(value))
- value = deepCopyList(value)
+ value = deep_copy_list(value)
.[key] = value
if(islist(key))
- key = deepCopyList(key)
+ key = deep_copy_list(key)
.[i] = key
.[key] = value
diff --git a/code/__HELPERS/logging.dm b/code/__HELPERS/logging.dm
index 5ecbff1087..faf28a9ef6 100644
--- a/code/__HELPERS/logging.dm
+++ b/code/__HELPERS/logging.dm
@@ -286,6 +286,16 @@ GLOBAL_PROTECT(config_error_log)
WRITE_LOG(GLOB.config_error_log, text)
SEND_TEXT(world.log, text)
+/// Logging for mapping errors
+/proc/log_mapping(text, skip_world_log)
+#ifdef UNIT_TESTS
+ GLOB.unit_test_mapping_logs += text
+#endif
+ if(skip_world_log)
+ return
+ WRITE_LOG(GLOB.mapping_log, text)
+ SEND_TEXT(world.log, text)
+
/proc/log_admin_private(text)
log_admin(text)
diff --git a/code/__HELPERS/string_lists.dm b/code/__HELPERS/string_lists.dm
new file mode 100644
index 0000000000..076bbf6427
--- /dev/null
+++ b/code/__HELPERS/string_lists.dm
@@ -0,0 +1,23 @@
+GLOBAL_LIST_EMPTY(string_lists)
+
+/**
+ * Caches lists with non-numeric stringify-able values (text or typepath).
+ */
+/proc/string_list(list/values)
+ var/string_id = values.Join("-")
+
+ . = GLOB.string_lists[string_id]
+
+ if(.)
+ return .
+
+ return GLOB.string_lists[string_id] = values
+
+///A wrapper for baseturf string lists, to offer support of non list values, and a stack_trace if we have major issues
+/proc/baseturfs_string_list(list/values, turf/baseturf_holder)
+ if(!islist(values))
+ return values //baseturf things
+ // return values
+ if(length(values) > 10)
+ return string_list(list(/turf/closed/cordon/debug))
+ return string_list(values)
diff --git a/code/__HELPERS/text.dm b/code/__HELPERS/text.dm
index d4d9eb3206..bde59bab86 100644
--- a/code/__HELPERS/text.dm
+++ b/code/__HELPERS/text.dm
@@ -196,6 +196,24 @@
return ""
+//Returns a string with reserved characters and spaces after the first and last letters removed
+//Like trim(), but very slightly faster. worth it for niche usecases
+/proc/trim_reduced(text)
+ var/starting_coord = 1
+ var/text_len = length(text)
+ for (var/i in 1 to text_len)
+ if (text2ascii(text, i) > 32)
+ starting_coord = i
+ break
+
+ for (var/i = text_len, i >= starting_coord, i--)
+ if (text2ascii(text, i) > 32)
+ return copytext(text, starting_coord, i + 1)
+
+ if(starting_coord > 1)
+ return copytext(text, starting_coord)
+ return ""
+
//Returns a string with reserved characters and spaces before the first word and after the last word removed.
/proc/trim(text)
return trim_left(trim_right(text))
diff --git a/code/__HELPERS/unsorted.dm b/code/__HELPERS/unsorted.dm
index 60fa224ef8..05e9b13858 100644
--- a/code/__HELPERS/unsorted.dm
+++ b/code/__HELPERS/unsorted.dm
@@ -888,7 +888,7 @@
animation.master = target
flick(flick_anim, animation)
-//Will return the contents of an atom recursivly to a depth of 'searchDepth'
+///Will return the contents of an atom recursivly to a depth of 'searchDepth', not including starting atom
/atom/proc/GetAllContents(searchDepth = 5, list/toReturn = list())
for(var/atom/part as anything in contents)
toReturn += part
@@ -896,6 +896,16 @@
part.GetAllContents(searchDepth - 1, toReturn)
return toReturn
+///Returns the src and all recursive contents as a list. Includes the starting atom.
+/atom/proc/get_all_contents(ignore_flag_1)
+ . = list(src)
+ var/i = 0
+ while(i < length(.))
+ var/atom/checked_atom = .[++i]
+ if(checked_atom.flags_atom & ignore_flag_1)
+ continue
+ . += checked_atom.contents
+
/// Returns list of contents of a turf recursively, much like GetAllContents
/// We only get containing atoms in the turf, excluding multitiles bordering on it
/turf/proc/GetAllTurfStrictContents(searchDepth = 5, list/toReturn = list())
@@ -2022,8 +2032,6 @@ GLOBAL_DATUM_INIT(dview_mob, /mob/dview, new)
return list(region_x1 & region_x2, region_y1 & region_y2)
-#define TURF_FROM_COORDS_LIST(List) (locate(List[1], List[2], List[3]))
-
//Vars that will not be copied when using /DuplicateObject
GLOBAL_LIST_INIT(duplicate_forbidden_vars,list(
"tag", "datum_components", "area", "type", "loc", "locs", "vars", "parent", "parent_type", "verbs", "ckey", "key",
diff --git a/code/_globalvars/bitfields.dm b/code/_globalvars/bitfields.dm
index 541d1a0536..1207f7969e 100644
--- a/code/_globalvars/bitfields.dm
+++ b/code/_globalvars/bitfields.dm
@@ -152,6 +152,12 @@ DEFINE_BITFIELD(flags_atom, list(
"USES_HEARING" = USES_HEARING,
))
+DEFINE_BITFIELD(turf_flags, list(
+ "TURF_ORGANIC" = TURF_ORGANIC,
+ "UNUSED_RESERVATION_TURF" = UNUSED_RESERVATION_TURF,
+ "RESERVATION_TURF" = RESERVATION_TURF,
+))
+
DEFINE_BITFIELD(flags_item, list(
"NODROP" = NODROP,
"NOBLUDGEON" = NOBLUDGEON,
diff --git a/code/_globalvars/global_lists.dm b/code/_globalvars/global_lists.dm
index c2cfb8263f..ee444cf63f 100644
--- a/code/_globalvars/global_lists.dm
+++ b/code/_globalvars/global_lists.dm
@@ -37,6 +37,9 @@ GLOBAL_LIST_EMPTY(minimap_icons)
GLOBAL_LIST_EMPTY(mainship_pipes)
+/// List of all the maps that have been cached for /proc/load_map
+GLOBAL_LIST_EMPTY(cached_maps)
+
/proc/initiate_minimap_icons()
var/list/icons = list()
for(var/iconstate in icon_states('icons/UI_icons/map_blips.dmi'))
diff --git a/code/_onclick/hud/map_popups.dm b/code/_onclick/hud/map_popups.dm
index aed6b46a79..26dc93bbff 100644
--- a/code/_onclick/hud/map_popups.dm
+++ b/code/_onclick/hud/map_popups.dm
@@ -118,10 +118,11 @@
* anyway. they're effectively qdel'd.
*/
/client/proc/clear_map(map_name)
- if(!map_name || !(map_name in screen_maps))
+ if(!map_name || !screen_maps[map_name])
return FALSE
for(var/atom/movable/screen/screen_obj in screen_maps[map_name])
screen_maps[map_name] -= screen_obj
+ remove_from_screen(screen_obj)
if(screen_obj.del_on_map_removal)
qdel(screen_obj)
screen_maps -= map_name
diff --git a/code/_onclick/hud/rendering/plane_master.dm b/code/_onclick/hud/rendering/plane_master.dm
index c337ee198e..4269814ce4 100644
--- a/code/_onclick/hud/rendering/plane_master.dm
+++ b/code/_onclick/hud/rendering/plane_master.dm
@@ -149,6 +149,14 @@
remove_filter("AO")
add_filter("AO", 1, drop_shadow_filter(x = 0, y = -2, size = 4, color = "#04080FAA"))
+/atom/movable/screen/plane_master/nvg_plane
+ name = "NVG plane"
+ plane = NVG_PLANE
+ render_relay_plane = RENDER_PLANE_GAME
+ blend_mode_override = BLEND_MULTIPLY
+ //icon = 'icons/mob/hud/screen1.dmi'
+ //icon_state = "noise"
+
/atom/movable/screen/plane_master/fullscreen
name = "fullscreen alert plane"
plane = FULLSCREEN_PLANE
diff --git a/code/_onclick/hud/rendering/plane_master_controller.dm b/code/_onclick/hud/rendering/plane_master_controller.dm
index 3548f22497..6b2f276ce7 100644
--- a/code/_onclick/hud/rendering/plane_master_controller.dm
+++ b/code/_onclick/hud/rendering/plane_master_controller.dm
@@ -51,11 +51,11 @@ INITIALIZE_IMMEDIATE(/atom/movable/plane_master_controller)
. += pm_iterator.get_filter(name)
///Transitions all filters owned by this plane master controller
-/atom/movable/plane_master_controller/transition_filter(name, time, list/new_params, easing, loop)
+/atom/movable/plane_master_controller/transition_filter(name, list/new_params, time, easing, loop)
. = ..()
for(var/i in controlled_planes)
var/atom/movable/screen/plane_master/pm_iterator = controlled_planes[i]
- pm_iterator.transition_filter(name, time, new_params, easing, loop)
+ pm_iterator.transition_filter(name, new_params, time, easing, loop)
/atom/movable/plane_master_controller/game
diff --git a/code/controllers/subsystem/atoms.dm b/code/controllers/subsystem/atoms.dm
index 3d544dca13..cc650ec2ae 100644
--- a/code/controllers/subsystem/atoms.dm
+++ b/code/controllers/subsystem/atoms.dm
@@ -19,6 +19,9 @@ SUBSYSTEM_DEF(atoms)
var/list/BadInitializeCalls = list()
+ ///initAtom() adds the atom its creating to this list iff InitializeAtoms() has been given a list to populate as an argument
+ var/list/created_atoms
+
initialized = INITIALIZATION_INSSATOMS
/datum/controller/subsystem/atoms/Initialize(timeofday)
@@ -34,7 +37,7 @@ SUBSYSTEM_DEF(atoms)
populate_seed_list()
return SS_INIT_SUCCESS
-/datum/controller/subsystem/atoms/proc/InitializeAtoms(list/atoms)
+/datum/controller/subsystem/atoms/proc/InitializeAtoms(list/atoms, list/atoms_to_return)
if(initialized == INITIALIZATION_INSSATOMS)
return
@@ -73,7 +76,10 @@ SUBSYSTEM_DEF(atoms)
processing_late_loaders = FALSE
/// Actually creates the list of atoms. Exists soley so a runtime in the creation logic doesn't cause initalized to totally break
-/datum/controller/subsystem/atoms/proc/CreateAtoms(list/atoms)
+/datum/controller/subsystem/atoms/proc/CreateAtoms(list/atoms, list/atoms_to_return = null)
+ if (atoms_to_return)
+ LAZYINITLIST(created_atoms)
+
#ifdef TESTING
var/count
#endif
@@ -152,12 +158,10 @@ SUBSYSTEM_DEF(atoms)
qdeleted = TRUE
else if(!(A.flags_atom & INITIALIZED))
BadInitializeCalls[the_type] |= BAD_INIT_DIDNT_INIT
- /*
else
- SEND_SIGNAL(A,COMSIG_ATOM_AFTER_SUCCESSFUL_INITIALIZE)
+ SEND_SIGNAL(A, COMSIG_ATOM_AFTER_SUCCESSFUL_INITIALIZED_ON)
if(created_atoms && from_template && ispath(the_type, /atom/movable))//we only want to populate the list with movables
created_atoms += A.get_all_contents()
- */
return qdeleted || QDELING(A)
diff --git a/code/controllers/subsystem/interior.dm b/code/controllers/subsystem/interior.dm
index 8fb7ffbfee..e2b845f833 100644
--- a/code/controllers/subsystem/interior.dm
+++ b/code/controllers/subsystem/interior.dm
@@ -15,11 +15,11 @@ SUBSYSTEM_DEF(interior)
var/height_to_request = template.height + INTERIOR_BORDER_SIZE
var/width_to_request = template.width + INTERIOR_BORDER_SIZE
- var/datum/turf_reservation/reserved_area = SSmapping.RequestBlockReservation(width_to_request, height_to_request, type = /datum/turf_reservation/interior)
+ var/datum/turf_reservation/reserved_area = SSmapping.request_turf_block_reservation(width_to_request, height_to_request, reservation_type = /datum/turf_reservation/interior)
- var/list/bottom_left = reserved_area.bottom_left_coords
+ var/turf/bottom_left = reserved_area.bottom_left_turfs[1]
- var/list/bounds = template.load(locate(bottom_left[1] + (INTERIOR_BORDER_SIZE / 2), bottom_left[2] + (INTERIOR_BORDER_SIZE / 2), bottom_left[3]), centered = FALSE)
+ var/list/bounds = template.load(locate(bottom_left.x + (INTERIOR_BORDER_SIZE / 2), bottom_left.y + (INTERIOR_BORDER_SIZE / 2), bottom_left.z), centered = FALSE)
var/list/turfs = block( locate(bounds[MAP_MINX], bounds[MAP_MINY], bounds[MAP_MINZ]),
locate(bounds[MAP_MAXX], bounds[MAP_MAXY], bounds[MAP_MAXZ]))
@@ -51,10 +51,11 @@ SUBSYSTEM_DEF(interior)
if(!isturf(loc))
loc = get_turf(loc)
- var/datum/weakref/reservation = SSmapping.used_turfs[loc]
+ var/datum/turf_reservation/interior/reservation = SSmapping.used_turfs[loc]
if(!istype(reservation))
return FALSE
+
return TRUE
#undef INTERIOR_BORDER_SIZE
diff --git a/code/controllers/subsystem/mapping.dm b/code/controllers/subsystem/mapping.dm
index 2c69f373c6..be6709f08c 100644
--- a/code/controllers/subsystem/mapping.dm
+++ b/code/controllers/subsystem/mapping.dm
@@ -1,7 +1,7 @@
SUBSYSTEM_DEF(mapping)
name = "Mapping"
init_order = SS_INIT_MAPPING
- flags = SS_NO_FIRE
+ runlevels = ALL
var/list/datum/map_config/configs
var/list/datum/map_config/next_map_configs
@@ -19,16 +19,24 @@ SUBSYSTEM_DEF(mapping)
var/list/turf/unused_turfs = list() //Not actually unused turfs they're unused but reserved for use for whatever requests them. "[zlevel_of_turf]" = list(turfs)
var/list/datum/turf_reservations //list of turf reservations
var/list/used_turfs = list() //list of turf = datum/turf_reservation
+ /// List of lists of turfs to reserve
+ var/list/lists_to_reserve = list()
var/list/reservation_ready = list()
var/clearing_reserved_turfs = FALSE
// Z-manager stuff
var/ground_start // should only be used for maploading-related tasks
- var/list/z_list
+ ///list of all z level datums in the order of their z (z level 1 is at index 1, etc.)
+ var/list/datum/space_level/z_list
var/datum/space_level/transit
var/num_of_res_levels = 1
+ /// True when in the process of adding a new Z-level, global locking
+ var/adding_new_zlevel = FALSE
+ /// list of traits and their associated z leves
+ var/list/z_trait_levels = list()
+
//dlete dis once #39770 is resolved
/datum/controller/subsystem/mapping/proc/HACK_LoadMapConfig()
if(!configs)
@@ -52,32 +60,67 @@ SUBSYSTEM_DEF(mapping)
loadWorld()
repopulate_sorted_areas()
preloadTemplates()
- // Add the transit level
- transit = add_new_zlevel("Transit/Reserved", list(ZTRAIT_RESERVED = TRUE))
- initialize_reserved_level(transit.z_value)
+ // Add the first transit level
+ var/datum/space_level/base_transit = add_reservation_zlevel()
+ initialize_reserved_level(base_transit.z_value)
repopulate_sorted_areas()
- for(var/maptype as anything in configs)
- var/datum/map_config/MC = configs[maptype]
- if(MC.perf_mode)
- GLOB.perf_flags |= MC.perf_mode
return SS_INIT_SUCCESS
+/datum/controller/subsystem/mapping/fire(resumed)
+ // Cache for sonic speed
+ var/list/unused_turfs = src.unused_turfs
+ // CM TODO: figure out if these 2 are needed. Might be required by updated versions of map reader
+ //var/list/world_contents = GLOB.areas_by_type[world.area].contents
+ //var/list/world_turf_contents = GLOB.areas_by_type[world.area].contained_turfs
+ var/list/lists_to_reserve = src.lists_to_reserve
+ var/index = 0
+ while(index < length(lists_to_reserve))
+ var/list/packet = lists_to_reserve[index + 1]
+ var/packetlen = length(packet)
+ while(packetlen)
+ if(MC_TICK_CHECK)
+ if(index)
+ lists_to_reserve.Cut(1, index)
+ return
+ var/turf/T = packet[packetlen]
+ T.empty(RESERVED_TURF_TYPE, RESERVED_TURF_TYPE, null, TRUE)
+ LAZYINITLIST(unused_turfs["[T.z]"])
+ unused_turfs["[T.z]"] |= T
+ //var/area/old_area = T.loc
+ //old_area.turfs_to_uncontain += T
+ T.turf_flags = UNUSED_RESERVATION_TURF
+ //world_contents += T
+ //world_turf_contents += T
+ packet.len--
+ packetlen = length(packet)
+
+ index++
+ lists_to_reserve.Cut(1, index)
+
/datum/controller/subsystem/mapping/proc/wipe_reservations(wipe_safety_delay = 100)
if(clearing_reserved_turfs || !initialized) //in either case this is just not needed.
return
clearing_reserved_turfs = TRUE
message_admins("Clearing dynamic reservation space.")
+ // /tg/ Shuttles have extra handling here to avoid them being desallocated
do_wipe_turf_reservations()
clearing_reserved_turfs = FALSE
+/datum/controller/subsystem/mapping/proc/get_reservation_from_turf(turf/T)
+ RETURN_TYPE(/datum/turf_reservation)
+ return used_turfs[T]
+
/datum/controller/subsystem/mapping/Recover()
flags |= SS_NO_INIT
initialized = SSmapping.initialized
map_templates = SSmapping.map_templates
+
+ shuttle_templates = SSmapping.shuttle_templates
unused_turfs = SSmapping.unused_turfs
turf_reservations = SSmapping.turf_reservations
used_turfs = SSmapping.used_turfs
+ areas_in_z = SSmapping.areas_in_z
configs = SSmapping.configs
next_map_configs = SSmapping.next_map_configs
@@ -121,18 +164,33 @@ SUBSYSTEM_DEF(mapping)
var/start_z = world.maxz + 1
var/i = 0
for (var/level in traits)
- add_new_zlevel("[name][i ? " [i + 1]" : ""]", level)
+ add_new_zlevel("[name][i ? " [i + 1]" : ""]", level, contain_turfs = FALSE)
++i
+ // ================== CM Change ==================
+ // For some reason /tg/ SSmapping attempts to center the map in new Z-Level
+ // but because it's done before loading, it's calculated before performing
+ // X/Y world expansion. When loading a map bigger than world, this results
+ // in a negative offset and the start of the map to not be loaded.
+
// load the maps
for (var/datum/parsed_map/pm as anything in parsed_maps)
- var/cur_z = start_z + parsed_maps[pm]
- if (!pm.load(1, 1, cur_z, no_changeturf = TRUE))
+ var/bounds = pm.bounds
+ var/x_offset = 1
+ var/y_offset = 1
+ if(bounds && world.maxx > bounds[MAP_MAXX])
+ x_offset = round(world.maxx / 2 - bounds[MAP_MAXX] / 2) + 1
+ if(bounds && world.maxy > bounds[MAP_MAXY])
+ y_offset = round(world.maxy / 2 - bounds[MAP_MAXY] / 2) + 1
+ if (!pm.load(x_offset, y_offset, start_z + parsed_maps[pm], no_changeturf = TRUE, new_z = TRUE))
errorList |= pm.original_path
- if(istype(z_list[cur_z], /datum/space_level))
- var/datum/space_level/cur_level = z_list[cur_z]
- cur_level.x_bounds = pm.bounds[MAP_MAXX]
- cur_level.y_bounds = pm.bounds[MAP_MAXY]
+ // CM Snowflake for Mass Screenshot dimensions auto detection
+ for(var/z in bounds[MAP_MINZ] to bounds[MAP_MAXZ])
+ var/datum/space_level/zlevel = z_list[start_z + z - 1]
+ zlevel.bounds = list(bounds[MAP_MINX], bounds[MAP_MINY], z, bounds[MAP_MAXX], bounds[MAP_MAXY], z)
+
+ // =============== END CM Change =================
+
if(!silent)
INIT_ANNOUNCE("Loaded [name] in [(REALTIMEOFDAY - start_time)/10]s!")
return parsed_maps
@@ -252,66 +310,78 @@ SUBSYSTEM_DEF(mapping)
var/datum/map_template/tent/new_tent = new template()
tent_type_templates[new_tent.map_id] = new_tent
-/datum/controller/subsystem/mapping/proc/RequestBlockReservation(width, height, z, type = /datum/turf_reservation, turf_type_override)
- UNTIL(initialized && !clearing_reserved_turfs)
- var/datum/turf_reservation/reserve = new type
- if(turf_type_override)
+/// Adds a new reservation z level. A bit of space that can be handed out on request
+/// Of note, reservations default to transit turfs, to make their most common use, shuttles, faster
+/datum/controller/subsystem/mapping/proc/add_reservation_zlevel(for_shuttles)
+ num_of_res_levels++
+ return add_new_zlevel("Transit/Reserved #[num_of_res_levels]", list(ZTRAIT_RESERVED = TRUE))
+
+/// Requests a /datum/turf_reservation based on the given width, height, and z_size. You can specify a z_reservation to use a specific z level, or leave it null to use any z level.
+/datum/controller/subsystem/mapping/proc/request_turf_block_reservation(
+ width,
+ height,
+ z_size = 1,
+ z_reservation = null,
+ reservation_type = /datum/turf_reservation,
+ turf_type_override = null,
+)
+ UNTIL((!z_reservation || reservation_ready["[z_reservation]"]) && !clearing_reserved_turfs)
+ var/datum/turf_reservation/reserve = new reservation_type
+ if(!isnull(turf_type_override))
reserve.turf_type = turf_type_override
- if(!z)
+ if(!z_reservation)
for(var/i in levels_by_trait(ZTRAIT_RESERVED))
- if(reserve.Reserve(width, height, i))
+ if(reserve.reserve(width, height, z_size, i))
return reserve
//If we didn't return at this point, theres a good chance we ran out of room on the exisiting reserved z levels, so lets try a new one
- log_debug("Ran out of space in existing transit levels, adding a new one")
- num_of_res_levels++
- var/datum/space_level/newReserved = add_new_zlevel("Transit/Reserved [num_of_res_levels]", list(ZTRAIT_RESERVED = TRUE))
+ var/datum/space_level/newReserved = add_reservation_zlevel()
initialize_reserved_level(newReserved.z_value)
- for(var/i in levels_by_trait(ZTRAIT_RESERVED))
- if(reserve.Reserve(width, height, i))
- return reserve
- CRASH("Despite adding a fresh reserved zlevel still failed to get a reservation")
+ if(reserve.reserve(width, height, z_size, newReserved.z_value))
+ return reserve
else
- if(!level_trait(z, ZTRAIT_RESERVED))
- log_debug("Cannot block reserve on a non-ZTRAIT_RESERVED level")
+ if(!level_trait(z_reservation, ZTRAIT_RESERVED))
qdel(reserve)
return
else
- if(reserve.Reserve(width, height, z))
+ if(reserve.reserve(width, height, z_size, z_reservation))
return reserve
- log_debug("unknown reservation failure")
QDEL_NULL(reserve)
-//This is not for wiping reserved levels, use wipe_reservations() for that.
+///Sets up a z level as reserved
+///This is not for wiping reserved levels, use wipe_reservations() for that.
+///If this is called after SSatom init, it will call Initialize on all turfs on the passed z, as its name promises
/datum/controller/subsystem/mapping/proc/initialize_reserved_level(z)
UNTIL(!clearing_reserved_turfs) //regardless, lets add a check just in case.
clearing_reserved_turfs = TRUE //This operation will likely clear any existing reservations, so lets make sure nothing tries to make one while we're doing it.
if(!level_trait(z,ZTRAIT_RESERVED))
clearing_reserved_turfs = FALSE
CRASH("Invalid z level prepared for reservations.")
- var/turf/A = get_turf(locate(8,8,z))
- var/turf/B = get_turf(locate(world.maxx - 8,world.maxy - 8,z))
+ var/turf/A = get_turf(locate(SHUTTLE_TRANSIT_BORDER,SHUTTLE_TRANSIT_BORDER,z))
+ var/turf/B = get_turf(locate(world.maxx - SHUTTLE_TRANSIT_BORDER,world.maxy - SHUTTLE_TRANSIT_BORDER,z))
var/block = block(A, B)
- for(var/t in block)
- // No need to empty() these, because it's world init and they're
- // already /turf/open/space/basic.
- var/turf/T = t
- T.flags_atom |= UNUSED_RESERVATION_TURF
+ for(var/turf/T as anything in block)
+ // No need to empty() these, because they just got created and are already /turf/open/space/basic.
+ T.turf_flags = UNUSED_RESERVATION_TURF
+ CHECK_TICK
+
+ // Gotta create these suckers if we've not done so already
+ if(SSatoms.initialized)
+ SSatoms.InitializeAtoms(Z_TURFS(z))
+
unused_turfs["[z]"] = block
reservation_ready["[z]"] = TRUE
clearing_reserved_turfs = FALSE
-/datum/controller/subsystem/mapping/proc/reserve_turfs(list/turfs)
- for(var/i in turfs)
- var/turf/T = i
- T.empty(RESERVED_TURF_TYPE, RESERVED_TURF_TYPE, null, TRUE)
- LAZYINITLIST(unused_turfs["[T.z]"])
- unused_turfs["[T.z]"] |= T
- T.flags_atom |= UNUSED_RESERVATION_TURF
- GLOB.areas_by_type[world.area].contents += T
- CHECK_TICK
+/// Schedules a group of turfs to be handed back to the reservation system's control
+/// If await is true, will sleep until the turfs are finished work
+/datum/controller/subsystem/mapping/proc/reserve_turfs(list/turfs, await = FALSE)
+ lists_to_reserve += list(turfs)
+ if(await)
+ UNTIL(!length(turfs))
//DO NOT CALL THIS PROC DIRECTLY, CALL wipe_reservations().
/datum/controller/subsystem/mapping/proc/do_wipe_turf_reservations()
+ PRIVATE_PROC(TRUE)
UNTIL(initialized) //This proc is for AFTER init, before init turf reservations won't even exist and using this will likely break things.
for(var/i in turf_reservations)
var/datum/turf_reservation/TR = i
@@ -319,19 +389,28 @@ SUBSYSTEM_DEF(mapping)
qdel(TR, TRUE)
UNSETEMPTY(turf_reservations)
var/list/clearing = list()
- for(var/l in unused_turfs) //unused_turfs is a assoc list by z = list(turfs)
+ for(var/l in unused_turfs) //unused_turfs is an assoc list by z = list(turfs)
if(islist(unused_turfs[l]))
clearing |= unused_turfs[l]
clearing |= used_turfs //used turfs is an associative list, BUT, reserve_turfs() can still handle it. If the code above works properly, this won't even be needed as the turfs would be freed already.
unused_turfs.Cut()
used_turfs.Cut()
- reserve_turfs(clearing)
+ reserve_turfs(clearing, await = TRUE)
/datum/controller/subsystem/mapping/proc/reg_in_areas_in_z(list/areas)
for(var/B in areas)
var/area/A = B
A.reg_in_areas_in_z()
+/// Takes a z level datum, and tells the mapping subsystem to manage it
+/// Also handles things like plane offset generation, and other things that happen on a z level to z level basis
+/datum/controller/subsystem/mapping/proc/manage_z_level(datum/space_level/new_z, filled_with_space, contain_turfs = TRUE)
+ // First, add the z
+ z_list += new_z
+ // Then we build our lookup lists
+ //var/z_value = new_z.z_value
+ //TODO: All the Z-plane init stuff goes below here normally, we don't have that yet
+
/// Gets a name for the marine ship as per the enabled ship map configuration
/datum/controller/subsystem/mapping/proc/get_main_ship_name()
if(!configs)
diff --git a/code/controllers/subsystem/shuttles.dm b/code/controllers/subsystem/shuttles.dm
index 3410473694..0348eddd99 100644
--- a/code/controllers/subsystem/shuttles.dm
+++ b/code/controllers/subsystem/shuttles.dm
@@ -1,65 +1,78 @@
#define MAX_TRANSIT_REQUEST_RETRIES 10
-#define SHUTTLE_SPAWN_BUFFER SSshuttle.wait * 10 /// Give a shuttle 10 "fires" (~10 seconds) to spawn before it can be cleaned up.
+/// How many turfs to allow before we stop blocking transit requests
+#define MAX_TRANSIT_TILE_COUNT (150 ** 2)
+/// How many turfs to allow before we start freeing up existing "soft reserved" transit docks
+/// If we're under load we want to allow for cycling, but if not we want to preserve already generated docks for use
+#define SOFT_TRANSIT_RESERVATION_THRESHOLD (100 ** 2)
+/// Give a shuttle 10 "fires" (~10 seconds) to spawn before it can be cleaned up.
+#define SHUTTLE_SPAWN_BUFFER SSshuttle.wait * 10
SUBSYSTEM_DEF(shuttle)
name = "Shuttle"
- wait = 10
+ wait = 1 SECONDS
init_order = SS_INIT_SHUTTLE
flags = SS_KEEP_TIMING
+ /// A list of all the mobile docking ports.
var/list/mobile = list()
+ /// A list of all the stationary docking ports.
var/list/stationary = list()
+ /// A list of all the transit docking ports.
var/list/transit = list()
- /// For ID generation
+ /// Now it's only for ID generation in /obj/docking_port/mobile/register()
var/list/assoc_mobile = list()
- /// For ID generation
+ /// Now it's only for ID generation in /obj/docking_port/stationary/register()
var/list/assoc_stationary = list()
+ /// A list of all the mobile docking ports currently requesting a spot in hyperspace.
var/list/transit_requesters = list()
+ /// An associative list of the mobile docking ports that have failed a transit request, with the amount of times they've actually failed that transit request, up to MAX_TRANSIT_REQUEST_RETRIES
var/list/transit_request_failures = list()
+ /// How many turfs our shuttles are currently utilizing in reservation space
+ var/transit_utilized = 0
var/obj/docking_port/mobile/vehicle_elevator/vehicle_elevator
var/list/hidden_shuttle_turfs = list() //all turfs hidden from navigation computers associated with a list containing the image hiding them and the type of the turf they are pretending to be
var/list/hidden_shuttle_turf_images = list() //only the images from the above list
- var/lockdown = FALSE //disallow transit after nuke goes off
+ /// Disallow transit after nuke goes off
+ var/lockdown = FALSE
+ /// The currently selected shuttle map_template in the shuttle manipulator's template viewer.
var/datum/map_template/shuttle/selected
+ /// The existing shuttle associated with the selected shuttle map_template.
var/obj/docking_port/mobile/existing_shuttle
- var/obj/docking_port/mobile/preview_shuttle
+ /// The shuttle map_template of the shuttle we want to preview.
var/datum/map_template/shuttle/preview_template
+ /// The docking port associated to the preview_template that's currently being previewed.
+ var/obj/docking_port/mobile/preview_shuttle
+ /// The turf reservation for the current previewed shuttle.
var/datum/turf_reservation/preview_reservation
- /// safety to stop shuttles loading over each other
+ /// Are we currently in the process of loading a shuttle? Useful to ensure we don't load more than one at once, to avoid weird inconsistencies and possible runtimes.
var/loading_shuttle = FALSE
/datum/controller/subsystem/shuttle/Initialize(timeofday)
- if(GLOB.perf_flags & PERF_TOGGLE_SHUTTLES)
- can_fire = FALSE
- return
initial_load()
return SS_INIT_SUCCESS
/datum/controller/subsystem/shuttle/proc/initial_load()
- for(var/s in stationary)
- var/obj/docking_port/stationary/S = s
- S.load_roundstart()
+ for(var/obj/docking_port/stationary/port as anything in stationary)
+ port.load_roundstart()
CHECK_TICK
/datum/controller/subsystem/shuttle/fire(resumed = FALSE)
- if(!resumed && (GLOB.perf_flags & PERF_TOGGLE_SHUTTLES))
- return
for(var/thing in mobile)
if(!thing)
mobile.Remove(thing)
continue
- var/obj/docking_port/mobile/P = thing
- P.check()
+ var/obj/docking_port/mobile/port = thing
+ port.check()
for(var/thing in transit)
var/obj/docking_port/stationary/transit/T = thing
if(!T.owner)
@@ -67,6 +80,11 @@ SUBSYSTEM_DEF(shuttle)
// This next one removes transit docks/zones that aren't
// immediately being used. This will mean that the zone creation
// code will be running a lot.
+
+ // If we're below the soft reservation threshold, don't clear the old space
+ // We're better off holding onto it for now
+ if(transit_utilized < SOFT_TRANSIT_RESERVATION_THRESHOLD)
+ continue
var/obj/docking_port/mobile/owner = T.owner
if(owner && (world.time > T.spawn_time + SHUTTLE_SPAWN_BUFFER))
var/idle = owner.mode == SHUTTLE_IDLE
@@ -78,7 +96,10 @@ SUBSYSTEM_DEF(shuttle)
if(!SSmapping.clearing_reserved_turfs)
while(transit_requesters.len)
var/requester = popleft(transit_requesters)
- var/success = generate_transit_dock(requester)
+ var/success = null
+ // Do not try and generate any transit if we're using more then our max already
+ if(transit_utilized < MAX_TRANSIT_TILE_COUNT)
+ success = generate_transit_dock(requester)
if(!success) // BACK OF THE QUEUE
transit_request_failures[requester]++
if(transit_request_failures[requester] < MAX_TRANSIT_REQUEST_RETRIES)
@@ -90,69 +111,74 @@ SUBSYSTEM_DEF(shuttle)
if(MC_TICK_CHECK)
break
-/datum/controller/subsystem/shuttle/proc/hasShuttle(id)
- for(var/obj/docking_port/mobile/M in mobile)
- if(M.id == id)
- return TRUE
- return FALSE
-
-/datum/controller/subsystem/shuttle/proc/getShuttle(id)
- for(var/obj/docking_port/mobile/M in mobile)
- if(M.id == id)
- return M
+/datum/controller/subsystem/shuttle/proc/getShuttle(id, warn = TRUE)
+ for(var/obj/docking_port/mobile/shuttle in mobile)
+ if(shuttle.id == id)
+ return shuttle
+ if(!warn)
+ return null
WARNING("couldn't find shuttle with id: [id]")
+/// Tries to get a shuttle based on its original template id (rather than one that may have an additional identifier)
+/datum/controller/subsystem/shuttle/proc/get_template_shuttle(id, warn = TRUE)
+ for(var/obj/docking_port/mobile/shuttle in mobile)
+ if(shuttle.template_id == id)
+ return shuttle
+ if(!warn)
+ return null
+ WARNING("couldn't find template shuttle with id: [id]")
+
/datum/controller/subsystem/shuttle/proc/getDock(id)
for(var/obj/docking_port/stationary/S in stationary)
if(S.id == id)
return S
WARNING("couldn't find dock with id: [id]")
-//try to move/request to dockHome if possible, otherwise dockAway. Mainly used for admin buttons
-/datum/controller/subsystem/shuttle/proc/toggleShuttle(shuttleId, dockHome, dockAway, timed)
- var/obj/docking_port/mobile/M = getShuttle(shuttleId)
- if(!M)
+//try to move/request to dock_home if possible, otherwise dock_away. Mainly used for admin buttons
+/datum/controller/subsystem/shuttle/proc/toggleShuttle(id, dock_home, dock_away, timed)
+ var/obj/docking_port/mobile/shuttle_port = getShuttle(id)
+ if(!shuttle_port)
return DOCKING_BLOCKED
- var/obj/docking_port/stationary/dockedAt = M.get_docked()
- var/destination = dockHome
- if(dockedAt && dockedAt.id == dockHome)
- destination = dockAway
+ var/obj/docking_port/stationary/docked_at = shuttle_port.get_docked()
+ var/destination = dock_home
+ if(docked_at && docked_at.id == dock_home)
+ destination = dock_away
if(timed)
- if(M.request(getDock(destination)))
+ if(shuttle_port.request(getDock(destination)))
return DOCKING_IMMOBILIZED
else
- if(M.initiate_docking(getDock(destination)) != DOCKING_SUCCESS)
+ if(shuttle_port.initiate_docking(getDock(destination)) != DOCKING_SUCCESS)
return DOCKING_IMMOBILIZED
- return DOCKING_SUCCESS //dock successful
+ return DOCKING_SUCCESS //dock successful
/**
* Moves a shuttle to a new location
*
* Arguments:
- * * shuttle_id - The ID of the shuttle (mobile docking port) to move
+ * * id - The ID of the shuttle (mobile docking port) to move
* * dock_id - The ID of the destination (stationary docking port) to move to
* * timed - If true, have the shuttle follow normal spool-up, jump, dock process. If false, immediately move to the new location.
*/
-/datum/controller/subsystem/shuttle/proc/moveShuttle(shuttleId, dockId, timed)
- var/obj/docking_port/stationary/D = getDock(dockId)
- var/obj/docking_port/mobile/M = getShuttle(shuttleId)
+/datum/controller/subsystem/shuttle/proc/moveShuttle(id, dock_id, timed)
+ var/obj/docking_port/mobile/shuttle_port = getShuttle(id)
+ var/obj/docking_port/stationary/docking_target = getDock(dock_id)
- return moveShuttleToDock(M, D, timed)
+ return moveShuttleToDock(shuttle_port, docking_target, timed)
-/datum/controller/subsystem/shuttle/proc/moveShuttleToDock(obj/docking_port/mobile/M, obj/docking_port/stationary/D, timed)
- if(!M)
+/datum/controller/subsystem/shuttle/proc/moveShuttleToDock(obj/docking_port/mobile/shuttle_port, obj/docking_port/stationary/docking_target, timed)
+ if(!shuttle_port)
return DOCKING_NULL_SOURCE
if(timed)
- if(M.request(D))
+ if(shuttle_port.request(docking_target))
return DOCKING_IMMOBILIZED
else
- if(M.initiate_docking(D) != DOCKING_SUCCESS)
+ if(shuttle_port.initiate_docking(docking_target) != DOCKING_SUCCESS)
return DOCKING_IMMOBILIZED
- return DOCKING_SUCCESS //dock successful
+ return DOCKING_SUCCESS //dock successful
/datum/controller/subsystem/shuttle/proc/request_transit_dock(obj/docking_port/mobile/M)
if(!istype(M))
- throw EXCEPTION("[M] is not a mobile docking port")
+ CRASH("[M] is not a mobile docking port")
if(M.assigned_transit)
return
@@ -164,7 +190,7 @@ SUBSYSTEM_DEF(shuttle)
// First, determine the size of the needed zone
// Because of shuttle rotation, the "width" of the shuttle is not
// always x.
- var/travel_dir = M.preferred_direction
+ //var/travel_dir = M.preferred_direction
// Remember, the direction is the direction we appear to be
// coming from
var/dock_angle = dir2angle(M.preferred_direction) + dir2angle(M.port_direction) + 180
@@ -188,31 +214,28 @@ SUBSYSTEM_DEF(shuttle)
[transit_height] in height. The travel dir is [travel_dir]."
*/
- var/transit_path = /turf/open/space/transit
- switch(travel_dir)
- if(NORTH)
- transit_path = /turf/open/space/transit/north
- if(SOUTH)
- transit_path = /turf/open/space/transit/south
- if(EAST)
- transit_path = /turf/open/space/transit/east
- if(WEST)
- transit_path = /turf/open/space/transit/west
+ var/transit_path = M.get_transit_path_type()
- var/datum/turf_reservation/proposal = SSmapping.RequestBlockReservation(transit_width, transit_height, null, /datum/turf_reservation/transit, transit_path)
+ var/datum/turf_reservation/proposal = SSmapping.request_turf_block_reservation(
+ transit_width,
+ transit_height,
+ z_size = 1, //if this is changed the turf uncontain code below has to be updated to support multiple zs
+ reservation_type = /datum/turf_reservation/transit,
+ turf_type_override = transit_path,
+ )
if(!istype(proposal))
log_debug("generate_transit_dock() failed to get a block reservation from mapping system")
return FALSE
- var/turf/bottomleft = locate(proposal.bottom_left_coords[1], proposal.bottom_left_coords[2], proposal.bottom_left_coords[3])
+ var/turf/bottomleft = proposal.bottom_left_turfs[1]
// Then create a transit docking port in the middle
var/coords = M.return_coords(0, 0, dock_dir)
- /* 0------2
- | |
- | |
- | x |
- 3------1
+ /* 0------2
+ * | |
+ * | |
+ * | x |
+ * 3------1
*/
var/x0 = coords[1]
@@ -229,23 +252,32 @@ SUBSYSTEM_DEF(shuttle)
var/turf/midpoint = locate(transit_x, transit_y, bottomleft.z)
if(!midpoint)
- log_debug("generate_transit_dock() failed to get a midpoint")
+ log_mapping("generate_transit_dock() failed to get a midpoint")
return FALSE
- var/area/shuttle/transit/A = new()
- //A.parallax_movedir = travel_dir
- A.contents = proposal.reserved_turfs
+ var/area/shuttle/transit/new_area = new()
+ //new_area.parallax_movedir = travel_dir
+ new_area.contents = proposal.reserved_turfs
var/obj/docking_port/stationary/transit/new_transit_dock = new(midpoint)
new_transit_dock.reserved_area = proposal
new_transit_dock.name = "Transit for [M.id]/[M.name]"
new_transit_dock.owner = M
- new_transit_dock.assigned_area = A
+ new_transit_dock.assigned_area = new_area
// Add 180, because ports point inwards, rather than outwards
new_transit_dock.setDir(angle2dir(dock_angle))
+ // Proposals use 2 extra hidden tiles of space, from the cordons that surround them
+ transit_utilized += (proposal.width + 2) * (proposal.height + 2)
M.assigned_transit = new_transit_dock
+ RegisterSignal(proposal, COMSIG_PARENT_QDELETING, PROC_REF(transit_space_clearing))
+
return new_transit_dock
+/// Gotta manage our space brother
+/datum/controller/subsystem/shuttle/proc/transit_space_clearing(datum/turf_reservation/source)
+ SIGNAL_HANDLER
+ transit_utilized -= (source.width + 2) * (source.height + 2)
+
/datum/controller/subsystem/shuttle/Recover()
if (istype(SSshuttle.mobile))
mobile = SSshuttle.mobile
@@ -269,7 +301,6 @@ SUBSYSTEM_DEF(shuttle)
preview_reservation = SSshuttle.preview_reservation
-
/datum/controller/subsystem/shuttle/proc/is_in_shuttle_bounds(atom/A)
var/area/current = get_area(A)
if(istype(current, /area/shuttle) && !istype(current, /area/shuttle/transit))
@@ -374,7 +405,15 @@ SUBSYSTEM_DEF(shuttle)
return shuttle
-/datum/controller/subsystem/shuttle/proc/action_load(datum/map_template/shuttle/loading_template, obj/docking_port/stationary/destination_port)
+/**
+ * Loads a shuttle template and sends it to a given destination port, optionally replacing the existing shuttle
+ *
+ * Arguments:
+ * * loading_template - The shuttle template to load
+ * * destination_port - The station docking port to send the shuttle to once loaded
+ * * to_replace - A shuttle to replace, otherwise we create a new one
+*/
+/datum/controller/subsystem/shuttle/proc/action_load(datum/map_template/shuttle/loading_template, obj/docking_port/stationary/destination_port, obj/docking_port/mobile/to_replace)
// Check for an existing preview
if(preview_shuttle && (loading_template != preview_template))
preview_shuttle.jumpToNullSpace()
@@ -383,56 +422,53 @@ SUBSYSTEM_DEF(shuttle)
QDEL_NULL(preview_reservation)
if(!preview_shuttle)
- if(load_template(loading_template))
- preview_shuttle.linkup(loading_template, destination_port)
+ load_template(loading_template)
preview_template = loading_template
// get the existing shuttle information, if any
var/timer = 0
var/mode = SHUTTLE_IDLE
- var/obj/docking_port/stationary/D
+ var/obj/docking_port/stationary/dest_dock
if(istype(destination_port))
- D = destination_port
- else if(existing_shuttle)
- timer = existing_shuttle.timer
- mode = existing_shuttle.mode
- D = existing_shuttle.get_docked()
+ dest_dock = destination_port
+ else if(to_replace)
+ timer = to_replace.timer
+ mode = to_replace.mode
+ dest_dock = to_replace.get_docked()
- if(!D)
- D = generate_transit_dock(preview_shuttle)
+ if(!dest_dock)
+ dest_dock = generate_transit_dock(preview_shuttle)
- if(!D)
- preview_shuttle.jumpToNullSpace()
+ if(!dest_dock)
CRASH("No dock found for preview shuttle ([preview_template.name]), aborting.")
- var/result = preview_shuttle.canDock(D)
+ var/result = preview_shuttle.canDock(dest_dock)
// truthy value means that it cannot dock for some reason
// but we can ignore the someone else docked error because we'll
// be moving into their place shortly
if((result != SHUTTLE_CAN_DOCK) && (result != SHUTTLE_SOMEONE_ELSE_DOCKED))
- WARNING("Template shuttle [preview_shuttle] cannot dock at [D] ([result]).")
+ WARNING("Template shuttle [preview_shuttle] cannot dock at [dest_dock] ([result]).")
return
- if(existing_shuttle)
- existing_shuttle.jumpToNullSpace()
-
- for(var/area/A as anything in preview_shuttle.shuttle_areas)
- for(var/turf/T as anything in A)
- // turfs inside the shuttle are not available for shuttles
- T.flags_atom &= ~UNUSED_RESERVATION_TURF
+ if(to_replace)
+ to_replace.jumpToNullSpace()
+ for(var/area/cur_area as anything in preview_shuttle.shuttle_areas)
+ for(var/turf/cur_turf as anything in cur_area)
// update underlays
- if(istype(T, /turf/closed/shuttle))
- var/dx = T.x - preview_shuttle.x
- var/dy = T.y - preview_shuttle.y
- var/turf/target_lz = locate(D.x + dx, D.y + dy, D.z)
- T.underlays.Cut()
- T.underlays += mutable_appearance(target_lz.icon, target_lz.icon_state, TURF_LAYER, FLOOR_PLANE)
-
+ if(istype(cur_turf, /turf/closed/shuttle))
+ var/dx = cur_turf.x - preview_shuttle.x
+ var/dy = cur_turf.y - preview_shuttle.y
+ var/turf/target_lz = locate(dest_dock.x + dx, dest_dock.y + dy, dest_dock.z)
+ cur_turf.underlays.Cut()
+ cur_turf.underlays += mutable_appearance(target_lz.icon, target_lz.icon_state, TURF_LAYER, FLOOR_PLANE)
+
+ preview_shuttle.register(to_replace != null)
var/list/force_memory = preview_shuttle.movement_force
preview_shuttle.movement_force = list("KNOCKDOWN" = 0, "THROW" = 0)
- preview_shuttle.initiate_docking(D)
+ preview_shuttle.mode = SHUTTLE_PREARRIVAL//No idle shuttle moving. Transit dock get removed if shuttle moves too long.
+ preview_shuttle.initiate_docking(dest_dock)
preview_shuttle.movement_force = force_memory
. = preview_shuttle
@@ -442,7 +478,7 @@ SUBSYSTEM_DEF(shuttle)
preview_shuttle.timer = timer
preview_shuttle.mode = mode
- preview_shuttle.register()
+ preview_shuttle.postregister(to_replace != null)
// TODO indicate to the user that success happened, rather than just
// blanking the modification tab
@@ -452,16 +488,28 @@ SUBSYSTEM_DEF(shuttle)
selected = null
QDEL_NULL(preview_reservation)
-/datum/controller/subsystem/shuttle/proc/load_template(datum/map_template/shuttle/S)
+/**
+ * Loads a shuttle template into the transit Z level, usually referred to elsewhere in the code as a shuttle preview.
+ * Does not register the shuttle so it can't be used yet, that's handled in action_load()
+ *
+ * Arguments:
+ * * loading_template - The shuttle template to load
+ */
+/datum/controller/subsystem/shuttle/proc/load_template(datum/map_template/shuttle/loading_template)
. = FALSE
- // load shuttle template, centred at shuttle import landmark,
- preview_reservation = SSmapping.RequestBlockReservation(S.width, S.height, SSmapping.transit.z_value, /datum/turf_reservation/transit)
+ // Load shuttle template to a fresh block reservation.
+ preview_reservation = SSmapping.request_turf_block_reservation(
+ loading_template.width,
+ loading_template.height,
+ 1,
+ reservation_type = /datum/turf_reservation/transit,
+ )
if(!preview_reservation)
CRASH("failed to reserve an area for shuttle template loading")
- var/turf/BL = TURF_FROM_COORDS_LIST(preview_reservation.bottom_left_coords)
- S.load(BL, centered = FALSE, register = FALSE)
+ var/turf/bottom_left = preview_reservation.bottom_left_turfs[1]
+ loading_template.load(bottom_left, centered = FALSE, register = FALSE)
- var/affected = S.get_affected_turfs(BL, centered=FALSE)
+ var/affected = loading_template.get_affected_turfs(bottom_left, centered=FALSE)
var/found = 0
// Search the turfs for docking ports
@@ -469,65 +517,86 @@ SUBSYSTEM_DEF(shuttle)
// the shuttle.
// - We need to check that no additional ports have slipped in from the
// template, because that causes unintended behaviour.
- for(var/T in affected)
- for(var/obj/docking_port/P in T)
- if(istype(P, /obj/docking_port/mobile))
+ for(var/affected_turfs in affected)
+ for(var/obj/docking_port/port in affected_turfs)
+ if(istype(port, /obj/docking_port/mobile))
found++
if(found > 1)
- qdel(P, force=TRUE)
- log_world("Map warning: Shuttle Template [S.mappath] has multiple mobile docking ports.")
+ qdel(port, force=TRUE)
+ log_mapping("Shuttle Template [loading_template.mappath] has multiple mobile docking ports.")
else
- preview_shuttle = P
- if(istype(P, /obj/docking_port/stationary))
- log_world("Map warning: Shuttle Template [S.mappath] has a stationary docking port.")
+ preview_shuttle = port
+ if(istype(port, /obj/docking_port/stationary))
+ log_mapping("Shuttle Template [loading_template.mappath] has a stationary docking port.")
if(!found)
- var/msg = "load_template(): Shuttle Template [S.mappath] has no mobile docking port. Aborting import."
- for(var/T in affected)
- var/turf/T0 = T
+ var/msg = "load_template(): Shuttle Template [loading_template.mappath] has no mobile docking port. Aborting import."
+ for(var/affected_turfs in affected)
+ var/turf/T0 = affected_turfs
T0.empty()
message_admins(msg)
WARNING(msg)
return
//Everything fine
- S.post_load(preview_shuttle)
+ loading_template.post_load(preview_shuttle)
return TRUE
+/**
+ * Removes the preview_shuttle from the transit Z-level
+ */
/datum/controller/subsystem/shuttle/proc/unload_preview()
if(preview_shuttle)
preview_shuttle.jumpToNullSpace()
preview_shuttle = null
+ if(preview_reservation)
+ QDEL_NULL(preview_reservation)
/datum/controller/subsystem/shuttle/ui_status(mob/user, datum/ui_state/state)
return UI_INTERACTIVE
+/datum/controller/subsystem/shuttle/ui_state(mob/user)
+ return GLOB.admin_state
+
/datum/controller/subsystem/shuttle/tgui_interact(mob/user, datum/tgui/ui)
ui = SStgui.try_update_ui(user, src, ui)
if(!ui)
- ui = new(user, src, "ShuttleManipulator", name, 800, 600)
+ ui = new(user, src, "ShuttleManipulator", name, 900, 600)
ui.open()
-
/datum/controller/subsystem/shuttle/ui_data(mob/user)
var/list/data = list()
// Templates panel
- data["selected"] = list()
+ data["templates"] = list()
+ var/list/templates = data["templates"]
+ //data["templates_tabs"] = list()
+ data["selected"] = null
- data["template_data"] = list()
- for(var/shuttle_id in SSmapping.shuttle_templates)
- var/datum/map_template/shuttle/S = SSmapping.shuttle_templates[shuttle_id]
- var/list/template_data = list()
- template_data["name"] = S.name
- template_data["shuttle_id"] = S.shuttle_id
- template_data["description"] = S.description
- template_data["admin_notes"] = S.admin_notes
+ for(var/id in SSmapping.shuttle_templates)
+ var/datum/map_template/shuttle/S = SSmapping.shuttle_templates[id]
+
+ if(!templates[S.port_id])
+ //data["templates_tabs"] += S.port_id
+ templates[S.port_id] = list(
+ "port_id" = S.port_id,
+ "templates" = list())
+
+ var/list/L = list()
+ L["name"] = S.name
+ L["id"] = S.shuttle_id
+ L["port_id"] = S.port_id
+ L["description"] = S.description
+ L["admin_notes"] = S.admin_notes
if(selected == S)
- data["selected"] = template_data
- data["template_data"] += list(template_data)
+ data["selected"] = L
+
+ templates[S.port_id]["templates"] += list(L)
+
+ //data["templates_tabs"] = sort_list(data["templates_tabs"])
data["existing_shuttle"] = null
+
// Status panel
data["shuttles"] = list()
for(var/i in mobile)
@@ -556,92 +625,118 @@ SUBSYSTEM_DEF(shuttle)
L["status"] = M.getDbgStatusText()
if(M == existing_shuttle)
data["existing_shuttle"] = L
- L["hijack"] = "N/A"
data["shuttles"] += list(L)
return data
-/datum/controller/subsystem/shuttle/ui_act(action, params)
- if(..())
+/datum/controller/subsystem/shuttle/ui_act(action, list/params, datum/tgui/ui, datum/ui_state/state)
+ . = ..()
+ if(.)
return
- var/mob/user = usr
+ var/mob/user = ui.user
// Preload some common parameters
- var/shuttle_id = params["shuttle_id"]
- var/datum/map_template/shuttle/S = SSmapping.shuttle_templates[shuttle_id]
+ var/id = params["id"]
+ var/datum/map_template/shuttle/template = SSmapping.shuttle_templates[id]
+ var/obj/docking_port/mobile/shuttle = getShuttle(id, warn = FALSE)
switch(action)
if("select_template")
- if(S)
- if(hasShuttle(S.shuttle_id))
- existing_shuttle = getShuttle(S.shuttle_id)
- else
- existing_shuttle = null
- selected = S
+ if(template)
+ existing_shuttle = shuttle
+ selected = template
+ if(!existing_shuttle)
+ existing_shuttle = get_template_shuttle(template.shuttle_id)
. = TRUE
+
if("jump_to")
- if(params["type"] == "mobile")
- for(var/i in mobile)
- var/obj/docking_port/mobile/M = i
- if(M.id == params["id"])
- user.forceMove(get_turf(M))
- . = TRUE
- break
+ if(shuttle)
+ user.client?.jump_to_object(shuttle)
+ . = TRUE
+
if("lock")
- for(var/i in mobile)
- var/obj/docking_port/mobile/M = i
- if(M.id == params["id"])
- . = TRUE
- var/obj/structure/machinery/computer/shuttle/console = M.getControlConsole()
- console.disable()
- message_admins("[key_name_admin(user)] set [M.id]'s disabled to TRUE.")
- break
+ if(shuttle)
+ . = TRUE
+ var/obj/structure/machinery/computer/shuttle/console = shuttle.getControlConsole()
+ console.disable()
+ message_admins("[key_name_admin(user)] set [id]'s disabled to TRUE.")
+
if("unlock")
- for(var/i in mobile)
- var/obj/docking_port/mobile/M = i
- if(M.id == params["id"])
- . = TRUE
- var/obj/structure/machinery/computer/shuttle/console = M.getControlConsole()
- console.enable()
- message_admins("[key_name_admin(user)] set [M.id]'s disabled to FALSE.")
- break
+ if(shuttle)
+ . = TRUE
+ var/obj/structure/machinery/computer/shuttle/console = shuttle.getControlConsole()
+ console.enable()
+ message_admins("[key_name_admin(user)] set [id]'s disabled to FALSE.")
+
if("fly")
- for(var/i in mobile)
- var/obj/docking_port/mobile/M = i
- if(M.id == params["id"])
- . = TRUE
- M.admin_fly_shuttle(user)
- break
+ if(shuttle)
+ . = TRUE
+ shuttle.admin_fly_shuttle(user)
if("fast_travel")
- for(var/i in mobile)
- var/obj/docking_port/mobile/M = i
- if(M.id == params["id"] && M.timer && M.timeLeft(1) >= 50)
- M.setTimer(50)
- . = TRUE
- message_admins("[key_name_admin(usr)] fast travelled [M]")
- log_admin("[key_name(usr)] fast travelled [M]")
- break
+ if(shuttle && shuttle.timer && shuttle.timeLeft(1) >= 50)
+ shuttle.setTimer(5 SECONDS)
+ . = TRUE
+ message_admins("[key_name_admin(user)] fast travelled [shuttle]")
+
+ if("load")
+ if(template)
+ if(loading_shuttle)
+ to_chat(user, SPAN_WARNING("Busy! Please wait..."))
+ return
+ . = TRUE
+ loading_shuttle = TRUE
+ // If successful, returns the mobile docking port
+ var/obj/docking_port/mobile/mdp = action_load(template)
+ if(mdp)
+ user.client?.jump_to_object(mdp)
+ message_admins("[key_name_admin(user)] loaded [mdp] with the shuttle manipulator.")
+ else
+ to_chat(user, SPAN_WARNING("Something went wrong. Check logs/STUI for more details."))
+ loading_shuttle = FALSE
if("preview")
- if(S)
+ if(template)
+ if(loading_shuttle)
+ to_chat(user, SPAN_WARNING("Busy! Please wait..."))
+ return
. = TRUE
+ loading_shuttle = TRUE
unload_preview()
- load_template(S)
+ load_template(template)
if(preview_shuttle)
- preview_template = S
- user.forceMove(get_turf(preview_shuttle))
- if("load")
- if(S)
+ preview_template = template
+ user.client?.jump_to_object(preview_shuttle)
+ loading_shuttle = FALSE
+
+ if("replace")
+ if(template)
+ if(loading_shuttle)
+ to_chat(user, SPAN_WARNING("Busy! Please wait..."))
+ return
. = TRUE
+ loading_shuttle = TRUE
+ var/to_replace = existing_shuttle
+ if(!existing_shuttle || tgui_alert(user, "Replace existing shuttle '[existing_shuttle.name]'?", "Replace shuttle?", list("Yes", "No")) != "Yes")
+ var/list/options = list()
+ for(var/obj/docking_port/mobile/option in mobile)
+ options["[option.name] ([option.id])"] = option
+ var/selection = tgui_input_list(user, "Replace some other shuttle instead?", "Replace shuttle?", options)
+ if(!selection)
+ loading_shuttle = FALSE
+ return
+ to_replace = options[selection]
// If successful, returns the mobile docking port
- var/obj/docking_port/mobile/mdp = action_load(S)
+ var/obj/docking_port/mobile/mdp = action_load(template, to_replace = to_replace)
if(mdp)
- user.forceMove(get_turf(mdp))
- message_admins("[key_name_admin(usr)] loaded [mdp] with the shuttle manipulator.")
- log_admin("[key_name(usr)] loaded [mdp] with the shuttle manipulator.")
-
+ user.client?.jump_to_object(mdp)
+ message_admins("[key_name_admin(user)] replaced [to_replace] with [mdp] with the shuttle manipulator.")
+ else
+ to_chat(user, SPAN_WARNING("Something went wrong. Check logs/STUI for more details."))
+ loading_shuttle = FALSE
-#undef SHUTTLE_SPAWN_BUFFER
+#undef MAX_TRANSIT_REQUEST_RETRIES
+#undef MAX_TRANSIT_TILE_COUNT
+#undef SOFT_TRANSIT_RESERVATION_THRESHOLD
diff --git a/code/datums/ammo/bullet/pistol.dm b/code/datums/ammo/bullet/pistol.dm
index 1c39d4ac99..3ce778bb1f 100644
--- a/code/datums/ammo/bullet/pistol.dm
+++ b/code/datums/ammo/bullet/pistol.dm
@@ -66,7 +66,7 @@
/datum/ammo/bullet/pistol/ap/toxin/on_hit_turf(turf/T, obj/projectile/P)
. = ..()
- if(T.flags_turf & TURF_ORGANIC)
+ if(T.turf_flags & TURF_ORGANIC)
P.damage *= organic_damage_mult
/datum/ammo/bullet/pistol/ap/toxin/on_hit_obj(obj/O, obj/projectile/P)
@@ -198,7 +198,7 @@
/datum/ammo/bullet/pistol/squash/toxin/on_hit_turf(turf/T, obj/projectile/P)
. = ..()
- if(T.flags_turf & TURF_ORGANIC)
+ if(T.turf_flags & TURF_ORGANIC)
P.damage *= organic_damage_mult
/datum/ammo/bullet/pistol/squash/toxin/on_hit_obj(obj/O, obj/projectile/P)
diff --git a/code/datums/ammo/bullet/revolver.dm b/code/datums/ammo/bullet/revolver.dm
index 654aceed7a..44f1eae96d 100644
--- a/code/datums/ammo/bullet/revolver.dm
+++ b/code/datums/ammo/bullet/revolver.dm
@@ -58,7 +58,7 @@
/datum/ammo/bullet/revolver/marksman/toxin/on_hit_turf(turf/T, obj/projectile/P)
. = ..()
- if(T.flags_turf & TURF_ORGANIC)
+ if(T.turf_flags & TURF_ORGANIC)
P.damage *= organic_damage_mult
/datum/ammo/bullet/revolver/marksman/toxin/on_hit_obj(obj/O, obj/projectile/P)
diff --git a/code/datums/ammo/bullet/rifle.dm b/code/datums/ammo/bullet/rifle.dm
index 46b655a428..263252df14 100644
--- a/code/datums/ammo/bullet/rifle.dm
+++ b/code/datums/ammo/bullet/rifle.dm
@@ -69,7 +69,7 @@
/datum/ammo/bullet/rifle/ap/toxin/on_hit_turf(turf/T, obj/projectile/P)
. = ..()
- if(T.flags_turf & TURF_ORGANIC)
+ if(T.turf_flags & TURF_ORGANIC)
P.damage *= organic_damage_mult
/datum/ammo/bullet/rifle/ap/toxin/on_hit_obj(obj/O, obj/projectile/P)
diff --git a/code/datums/ammo/bullet/smg.dm b/code/datums/ammo/bullet/smg.dm
index e24b3021da..3fa087972f 100644
--- a/code/datums/ammo/bullet/smg.dm
+++ b/code/datums/ammo/bullet/smg.dm
@@ -51,7 +51,7 @@
/datum/ammo/bullet/smg/ap/toxin/on_hit_turf(turf/T, obj/projectile/P)
. = ..()
- if(T.flags_turf & TURF_ORGANIC)
+ if(T.turf_flags & TURF_ORGANIC)
P.damage *= organic_damage_mult
/datum/ammo/bullet/smg/ap/toxin/on_hit_obj(obj/O, obj/projectile/P)
diff --git a/code/datums/components/phone.dm b/code/datums/components/phone.dm
index 3d0cfaf41f..67c926365b 100644
--- a/code/datums/components/phone.dm
+++ b/code/datums/components/phone.dm
@@ -26,7 +26,10 @@ GLOBAL_LIST_EMPTY_TYPED(phones, /datum/component/phone)
var/datum/component/phone/calling_phone
/// Whether or not the phone is receiving calls or not. Varies between on/off or forcibly on/off.
- var/do_not_disturb = PHONE_DO_NOT_DISTURB_OFF
+ var/do_not_disturb = PHONE_DND_OFF
+
+ /// The Phone_ID of the last person to call this telephone.
+ var/last_caller
/// The ID of our timer to cancel an attempted call and "go to voicemail"
var/timeout_timer_id
@@ -247,10 +250,10 @@ GLOBAL_LIST_EMPTY_TYPED(phones, /datum/component/phone)
if(phone_handset.loc)
return FALSE
- if(do_not_disturb == PHONE_DO_NOT_DISTURB_ON)
+ if(do_not_disturb == PHONE_DND_ON)
return FALSE
- if(do_not_disturb == PHONE_DO_NOT_DISTURB_FORCED)
+ if(do_not_disturb == PHONE_DND_FORCED)
return FALSE
return TRUE
@@ -296,6 +299,7 @@ GLOBAL_LIST_EMPTY_TYPED(phones, /datum/component/phone)
/// What we do when our phone is receiving a call, called from incoming_call's call_phone()
/datum/component/phone/proc/getting_call(datum/component/phone/incoming_call)
calling_phone = incoming_call
+ last_caller = incoming_call.phone_id
SEND_SIGNAL(holder, COMSIG_ATOM_PHONE_RINGING)
ringing_loop.start()
@@ -405,13 +409,13 @@ GLOBAL_LIST_EMPTY_TYPED(phones, /datum/component/phone)
hearing_mob.hear_radio(message, "says", message_language, part_a = "", part_b = " ", vname = name_override, speaker = speaker, command = loudness, no_paygrade = TRUE)
/// Toggles do not disturb on or off, does not handle forced or unable do_not_disturb variables
-/datum/component/phone/proc/toggle_do_not_disturb(mob/user)
+/datum/component/phone/proc/toggle_dnd(mob/user)
switch(do_not_disturb)
- if(PHONE_DO_NOT_DISTURB_ON)
- do_not_disturb = PHONE_DO_NOT_DISTURB_OFF
+ if(PHONE_DND_ON)
+ do_not_disturb = PHONE_DND_OFF
to_chat(user, SPAN_NOTICE("Do Not Disturb has been disabled. You can now receive calls."))
- if(PHONE_DO_NOT_DISTURB_OFF)
- do_not_disturb = PHONE_DO_NOT_DISTURB_ON
+ if(PHONE_DND_OFF)
+ do_not_disturb = PHONE_DND_ON
to_chat(user, SPAN_WARNING("Do Not Disturb has been enabled. No calls will be received."))
else
return FALSE
@@ -440,21 +444,22 @@ GLOBAL_LIST_EMPTY_TYPED(phones, /datum/component/phone)
call_phone(ui.user, params["phone_id"])
return TRUE
- if("toggle_do_not_disturb")
- toggle_do_not_disturb(ui.user)
+ if("toggle_dnd")
+ toggle_dnd(ui.user)
return TRUE
/datum/component/phone/ui_data(mob/user)
var/list/data = list()
- data["do_not_disturb"] = do_not_disturb
+ data["availability"] = do_not_disturb
+ data["last_caller"] = last_caller
return data
/datum/component/phone/ui_static_data(mob/user)
. = list()
- .["available_phones"] = get_phones() - list(phone_id)
+ .["available_transmitters"] = get_phones() - list(phone_id)
var/list/phones = list()
for(var/datum/component/phone/cycled_phone as anything in GLOB.phones)
phones += list(list(
@@ -464,7 +469,7 @@ GLOBAL_LIST_EMPTY_TYPED(phones, /datum/component/phone)
"phone_icon" = cycled_phone.phone_icon
))
- .["phones"] = phones
+ .["transmitters"] = phones
/datum/component/phone/tgui_interact(mob/user, datum/tgui/ui)
ui = SStgui.try_update_ui(user, src, ui)
@@ -526,10 +531,10 @@ GLOBAL_LIST_EMPTY_TYPED(phones, /datum/component/phone)
if(calling_phone)
return FALSE
- if(do_not_disturb == PHONE_DO_NOT_DISTURB_ON)
+ if(do_not_disturb == PHONE_DND_ON)
return FALSE
- if(do_not_disturb == PHONE_DO_NOT_DISTURB_FORCED)
+ if(do_not_disturb == PHONE_DND_FORCED)
return FALSE
return TRUE
diff --git a/code/datums/entities/player.dm b/code/datums/entities/player.dm
index ddb52cb8d0..4475fbf1ff 100644
--- a/code/datums/entities/player.dm
+++ b/code/datums/entities/player.dm
@@ -26,6 +26,9 @@
var/stickyban_whitelisted = FALSE
+ var/byond_account_age
+ var/first_join_date
+
// UNTRACKED FIELDS
var/name // Used for NanoUI statistics menu
@@ -74,6 +77,8 @@ BSQL_PROTECT_DATUM(/datum/entity/player)
"migrated_bans" = DB_FIELDTYPE_INT,
"migrated_jobbans" = DB_FIELDTYPE_INT,
"stickyban_whitelisted" = DB_FIELDTYPE_INT,
+ "byond_account_age" = DB_FIELDTYPE_STRING_MEDIUM,
+ "first_join_date" = DB_FIELDTYPE_STRING_MEDIUM,
)
// NOTE: good example of database operations using NDatabase, so it is well commented
@@ -454,6 +459,31 @@ BSQL_PROTECT_DATUM(/datum/entity/player)
for(var/datum/entity/player_stat/S as anything in _stat)
LAZYSET(stats, S.stat_id, S)
+/datum/entity/player/proc/load_byond_account_age()
+ var/datum/http_request/request = new()
+ request.prepare(RUSTG_HTTP_METHOD_GET, "https://www.byond.com/members/[ckey]?format=text")
+ request.execute_blocking()
+ var/datum/http_response/response = request.into_response()
+ if(response.errored)
+ return
+
+ var/static/regex/regex = regex("joined = \"(\\d{4}-\\d{2}-\\d{2})\"")
+ if(!regex.Find(response.body))
+ return
+
+ byond_account_age = regex.group[1]
+
+/datum/entity/player/proc/find_first_join_date()
+ var/list/triplets = search_login_triplet_by_ckey(ckey)
+
+ if(!length(triplets))
+ first_join_date = "UNKNOWN"
+ return
+
+ var/datum/view_record/login_triplet/first_triplet = triplets[1]
+ first_join_date = first_triplet.login_date
+
+
/proc/get_player_from_key(key)
var/safe_key = ckey(key)
if(!safe_key)
@@ -476,9 +506,15 @@ BSQL_PROTECT_DATUM(/datum/entity/player)
error("ALARM: MISMATCH. Loaded player data for client [ckey], player data ckey is [player.ckey], id: [player.id]")
player_data = player
player_data.owning_client = src
+ if(!player_data.last_login)
+ player_data.first_join_date = "[time2text(world.realtime, "YYYY-MM-DD hh:mm:ss")]"
+ if(!player_data.first_join_date)
+ player_data.find_first_join_date()
player_data.last_login = "[time2text(world.realtime, "YYYY-MM-DD hh:mm:ss")]"
player_data.last_known_ip = address
player_data.last_known_cid = computer_id
+ if(!player_data.byond_account_age)
+ player_data.load_byond_account_age()
player_data.save()
record_login_triplet(player.ckey, address, computer_id)
player_data.sync()
diff --git a/code/datums/mob_hud.dm b/code/datums/mob_hud.dm
index fdd4435d12..3f5d0fcc02 100644
--- a/code/datums/mob_hud.dm
+++ b/code/datums/mob_hud.dm
@@ -187,7 +187,7 @@ var/list/datum/mob_hud/huds = list(
//Factions
/datum/mob_hud/faction
- hud_icons = list(FACTION_HUD, ORDER_HUD)
+ hud_icons = list(FACTION_HUD, ORDER_HUD, HOLOCARD_HUD)
var/faction_to_check = FACTION_MARINE
/datum/mob_hud/faction/add_to_single_hud(mob/user, mob/target)
@@ -211,7 +211,7 @@ var/list/datum/mob_hud/huds = list(
faction_to_check = FACTION_PMC
/datum/mob_hud/faction/observer
- hud_icons = list(FACTION_HUD, ORDER_HUD, HUNTER_CLAN)
+ hud_icons = list(FACTION_HUD, ORDER_HUD, HUNTER_CLAN, HOLOCARD_HUD)
///////// MOB PROCS //////////////////////////////:
@@ -755,7 +755,13 @@ var/global/image/hud_icon_hudfocus
holder.overlays += hud_icon_hudfocus
hud_list[ORDER_HUD] = holder
+/mob/proc/hud_set_holocard()
+ return
+// HOLOCARD HUD
+/mob/living/carbon/human/hud_set_holocard()
+ var/image/holder = hud_list[HOLOCARD_HUD]
+ holder.icon_state = holo_card_color ? "holo_card_[holo_card_color]" : "hudblank"
// Xeno "hostile" HUD
/mob/living/carbon/human/proc/update_xeno_hostile_hud()
diff --git a/code/datums/shuttles.dm b/code/datums/shuttles.dm
index bf3644aca0..6a6bd61eb8 100644
--- a/code/datums/shuttles.dm
+++ b/code/datums/shuttles.dm
@@ -62,44 +62,30 @@
locate(.[MAP_MAXX], .[MAP_MAXY], .[MAP_MAXZ]))
for(var/i in 1 to turfs.len)
var/turf/place = turfs[i]
+
+ // ================== CM Change ==================
+ // We perform atom initialization of the docking_ports BEFORE skipping space,
+ // because our lifeboats have their corners as object props and still
+ // reside on space turfs. Notably the bottom left corner, which also contains
+ // the docking port.
+
+ for(var/obj/docking_port/mobile/port in place)
+ SSatoms.InitializeAtoms(list(port))
+ if(register)
+ port.register()
+
if(istype(place, /turf/open/space)) // This assumes all shuttles are loaded in a single spot then moved to their real destination.
continue
if(length(place.baseturfs) < 2) // Some snowflake shuttle shit
continue
place.baseturfs.Insert(3, /turf/baseturf_skipover/shuttle)
-
- for(var/obj/docking_port/mobile/port in place)
- if(register)
- port.register()
- if(isnull(port_x_offset))
- continue
- switch(port.dir) // Yeah this looks a little ugly but mappers had to do this in their head before
- if(NORTH)
- port.width = width
- port.height = height
- port.dwidth = port_x_offset - 1
- port.dheight = port_y_offset - 1
- if(EAST)
- port.width = height
- port.height = width
- port.dwidth = height - port_y_offset
- port.dheight = port_x_offset - 1
- if(SOUTH)
- port.width = width
- port.height = height
- port.dwidth = width - port_x_offset
- port.dheight = height - port_y_offset
- if(WEST)
- port.width = height
- port.height = width
- port.dwidth = port_y_offset - 1
- port.dheight = width - port_x_offset
+ // =============== END CM Change =================
//Whatever special stuff you want
-/datum/map_template/shuttle/proc/post_load(obj/docking_port/mobile/M)
+/datum/map_template/shuttle/post_load(obj/docking_port/mobile/M)
if(movement_force)
M.movement_force = movement_force.Copy()
-
+ M.linkup(src)
/datum/map_template/shuttle/vehicle
shuttle_id = MOBILE_SHUTTLE_VEHICLE_ELEVATOR
@@ -116,6 +102,12 @@
/datum/map_template/shuttle/trijent_elevator/B
elevator_network = "B"
+/datum/map_template/shuttle/trijent_elevator/post_load(obj/docking_port/mobile/M)
+ . = ..()
+ var/obj/docking_port/mobile/trijent_elevator/elev = M
+ elev.elevator_network = elevator_network
+ log_debug("Adding network [elevator_network] to [M.id]")
+
/datum/map_template/shuttle/trijent_elevator/ice_elevator
name = "Classic Ice Elevator"
shuttle_id = "ice_classic_shuttle"
diff --git a/code/datums/supply_packs/gear.dm b/code/datums/supply_packs/gear.dm
index 54a2ae221c..5343b93dab 100644
--- a/code/datums/supply_packs/gear.dm
+++ b/code/datums/supply_packs/gear.dm
@@ -63,3 +63,32 @@
containertype = /obj/structure/closet/crate/ammo
containername = "fulton recovery device crate"
group = "Gear"
+
+/datum/supply_packs/parachute
+ name = "parachute crate (x20)"
+ contains = list(
+ /obj/item/parachute,
+ /obj/item/parachute,
+ /obj/item/parachute,
+ /obj/item/parachute,
+ /obj/item/parachute,
+ /obj/item/parachute,
+ /obj/item/parachute,
+ /obj/item/parachute,
+ /obj/item/parachute,
+ /obj/item/parachute,
+ /obj/item/parachute,
+ /obj/item/parachute,
+ /obj/item/parachute,
+ /obj/item/parachute,
+ /obj/item/parachute,
+ /obj/item/parachute,
+ /obj/item/parachute,
+ /obj/item/parachute,
+ /obj/item/parachute,
+ /obj/item/parachute,
+ )
+ cost = 40
+ containertype = /obj/structure/closet/crate/supply
+ containername = "parachute crate"
+ group = "Gear"
diff --git a/code/game/atoms.dm b/code/game/atoms.dm
index 2f42c7891f..4ea51ef305 100644
--- a/code/game/atoms.dm
+++ b/code/game/atoms.dm
@@ -537,7 +537,7 @@ Parameters are passed from New.
onclose(usr, "[name]")
///This proc is called on atoms when they are loaded into a shuttle
-/atom/proc/connect_to_shuttle(obj/docking_port/mobile/port, obj/docking_port/stationary/dock, idnum, override=FALSE)
+/atom/proc/connect_to_shuttle(mapload, obj/docking_port/mobile/port, obj/docking_port/stationary/dock)
return
/**
@@ -568,7 +568,17 @@ Parameters are passed from New.
filters += filter(arglist(arguments))
UNSETEMPTY(filter_data)
-/atom/proc/transition_filter(name, time, list/new_params, easing, loop)
+/** Update a filter's parameter and animate this change. If the filter doesnt exist we won't do anything.
+ * Basically a [datum/proc/modify_filter] call but with animations. Unmodified filter parameters are kept.
+ *
+ * Arguments:
+ * * name - Filter name
+ * * new_params - New parameters of the filter
+ * * time - time arg of the BYOND animate() proc.
+ * * easing - easing arg of the BYOND animate() proc.
+ * * loop - loop arg of the BYOND animate() proc.
+ */
+/atom/proc/transition_filter(name, list/new_params, time, easing, loop)
var/filter = get_filter(name)
if(!filter)
return
diff --git a/code/game/camera_manager/camera_manager.dm b/code/game/camera_manager/camera_manager.dm
new file mode 100644
index 0000000000..95292830d4
--- /dev/null
+++ b/code/game/camera_manager/camera_manager.dm
@@ -0,0 +1,246 @@
+#define DEFAULT_MAP_SIZE 15
+
+#define RENDER_MODE_TARGET 1
+#define RENDER_MODE_AREA 2
+
+/datum/component/camera_manager
+ var/map_name
+ var/obj/structure/machinery/camera/current
+ var/datum/shape/rectangle/current_area
+ var/atom/movable/screen/map_view/cam_screen
+ var/atom/movable/screen/background/cam_background
+ var/list/range_turfs = list()
+ /// The turf where the camera was last updated.
+ var/turf/last_camera_turf
+ var/target_x
+ var/target_y
+ var/target_z
+ var/target_width
+ var/target_height
+ var/list/cam_plane_masters
+ var/isXRay = FALSE
+ var/render_mode = RENDER_MODE_TARGET
+
+/datum/component/camera_manager/Initialize()
+ . = ..()
+ map_name = "camera_manager_[REF(src)]_map"
+ cam_screen = new
+ cam_screen.icon = null
+ cam_screen.name = "screen"
+ cam_screen.assigned_map = map_name
+ cam_screen.del_on_map_removal = FALSE
+ cam_screen.screen_loc = "[map_name]:1,1"
+ cam_screen.appearance_flags |= TILE_BOUND
+ cam_background = new
+ cam_background.assigned_map = map_name
+ cam_background.del_on_map_removal = FALSE
+ cam_background.appearance_flags |= TILE_BOUND
+
+ cam_plane_masters = list()
+ for(var/plane in subtypesof(/atom/movable/screen/plane_master) - /atom/movable/screen/plane_master/blackness)
+ var/atom/movable/screen/plane_master/instance = new plane()
+ add_plane(instance)
+
+/datum/component/camera_manager/Destroy(force, ...)
+ . = ..()
+ range_turfs = null
+ current_area = null
+ QDEL_LIST_ASSOC_VAL(cam_plane_masters)
+ QDEL_NULL(cam_background)
+ QDEL_NULL(cam_screen)
+ if(current)
+ UnregisterSignal(current, COMSIG_PARENT_QDELETING)
+ current = null
+ last_camera_turf = null
+
+/datum/component/camera_manager/proc/add_plane(atom/movable/screen/plane_master/instance)
+ instance.assigned_map = map_name
+ instance.appearance_flags |= TILE_BOUND
+ instance.del_on_map_removal = FALSE
+ if(instance.blend_mode_override)
+ instance.blend_mode = instance.blend_mode_override
+ instance.screen_loc = "[map_name]:CENTER"
+ cam_plane_masters["[instance.plane]"] = instance
+
+/datum/component/camera_manager/proc/register(source, mob/user)
+ SIGNAL_HANDLER
+ var/client/user_client = user.client
+ if(!user_client)
+ return
+ user_client.register_map_obj(cam_screen)
+ user_client.register_map_obj(cam_background)
+ for(var/plane_id in cam_plane_masters)
+ user_client.register_map_obj(cam_plane_masters[plane_id])
+
+/datum/component/camera_manager/proc/unregister(source, mob/user)
+ SIGNAL_HANDLER
+ var/client/user_client = user.client
+ if(!user_client)
+ return
+ user_client.clear_map(map_name)
+
+/datum/component/camera_manager/RegisterWithParent()
+ . = ..()
+ SEND_SIGNAL(parent, COMSIG_CAMERA_MAPNAME_ASSIGNED, map_name)
+ RegisterSignal(parent, COMSIG_CAMERA_REGISTER_UI, PROC_REF(register))
+ RegisterSignal(parent, COMSIG_CAMERA_UNREGISTER_UI, PROC_REF(unregister))
+ RegisterSignal(parent, COMSIG_CAMERA_SET_NVG, PROC_REF(enable_nvg))
+ RegisterSignal(parent, COMSIG_CAMERA_CLEAR_NVG, PROC_REF(disable_nvg))
+ RegisterSignal(parent, COMSIG_CAMERA_SET_AREA, PROC_REF(set_camera_rect))
+ RegisterSignal(parent, COMSIG_CAMERA_SET_TARGET, PROC_REF(set_camera))
+ RegisterSignal(parent, COMSIG_CAMERA_CLEAR, PROC_REF(clear_camera))
+ RegisterSignal(parent, COMSIG_CAMERA_REFRESH, PROC_REF(refresh_camera))
+
+/datum/component/camera_manager/UnregisterFromParent()
+ . = ..()
+ UnregisterSignal(parent, COMSIG_CAMERA_REGISTER_UI)
+ UnregisterSignal(parent, COMSIG_CAMERA_UNREGISTER_UI)
+ UnregisterSignal(parent, COMSIG_CAMERA_SET_NVG)
+ UnregisterSignal(parent, COMSIG_CAMERA_CLEAR_NVG)
+ UnregisterSignal(parent, COMSIG_CAMERA_SET_AREA)
+ UnregisterSignal(parent, COMSIG_CAMERA_SET_TARGET)
+ UnregisterSignal(parent, COMSIG_CAMERA_CLEAR)
+ UnregisterSignal(parent, COMSIG_CAMERA_REFRESH)
+
+/datum/component/camera_manager/proc/clear_camera()
+ SIGNAL_HANDLER
+ if(current)
+ UnregisterSignal(current, COMSIG_PARENT_QDELETING)
+ current_area = null
+ current = null
+ target_x = null
+ target_y = null
+ target_z = null
+ target_width = null
+ target_height = null
+ show_camera_static()
+
+/datum/component/camera_manager/proc/refresh_camera()
+ SIGNAL_HANDLER
+ if(render_mode == RENDER_MODE_AREA)
+ update_area_camera()
+ return
+ update_target_camera()
+
+/datum/component/camera_manager/proc/set_camera(source, atom/target, w, h)
+ SIGNAL_HANDLER
+ render_mode = RENDER_MODE_TARGET
+ if(current)
+ UnregisterSignal(current, COMSIG_PARENT_QDELETING)
+ current = target
+ target_width = w
+ target_height = h
+ RegisterSignal(current, COMSIG_PARENT_QDELETING, PROC_REF(show_camera_static))
+ update_target_camera()
+
+/datum/component/camera_manager/proc/set_camera_rect(source, x, y, z, w, h)
+ SIGNAL_HANDLER
+ render_mode = RENDER_MODE_AREA
+ if(current)
+ UnregisterSignal(current, COMSIG_PARENT_QDELETING)
+ current = null
+ current_area = RECT(x, y, w, h)
+ target_x = x
+ target_y = y
+ target_z = z
+ target_width = w
+ target_height = h
+ update_area_camera()
+
+/datum/component/camera_manager/proc/enable_nvg(source, power, matrixcol)
+ SIGNAL_HANDLER
+ for(var/plane_id in cam_plane_masters)
+ var/atom/movable/screen/plane_master/plane = cam_plane_masters["[plane_id]"]
+ plane.add_filter("nvg", 1, color_matrix_filter(color_matrix_from_string(matrixcol)))
+ sync_lighting_plane_alpha(LIGHTING_PLANE_ALPHA_MOSTLY_INVISIBLE)
+
+/datum/component/camera_manager/proc/disable_nvg()
+ SIGNAL_HANDLER
+ for(var/plane_id in cam_plane_masters)
+ var/atom/movable/screen/plane_master/plane = cam_plane_masters["[plane_id]"]
+ plane.remove_filter("nvg")
+ sync_lighting_plane_alpha(LIGHTING_PLANE_ALPHA_VISIBLE)
+
+/datum/component/camera_manager/proc/sync_lighting_plane_alpha(lighting_alpha)
+ var/atom/movable/screen/plane_master/lighting/lighting = cam_plane_masters["[LIGHTING_PLANE]"]
+ if(lighting)
+ lighting.alpha = lighting_alpha
+ var/atom/movable/screen/plane_master/lighting/exterior_lighting = cam_plane_masters["[EXTERIOR_LIGHTING_PLANE]"]
+ if(exterior_lighting)
+ exterior_lighting.alpha = min(GLOB.minimum_exterior_lighting_alpha, lighting_alpha)
+
+/**
+ * Set the displayed camera to the static not-connected.
+ */
+/datum/component/camera_manager/proc/show_camera_static()
+ cam_screen.vis_contents.Cut()
+ last_camera_turf = null
+ cam_background.icon_state = "scanline2"
+ cam_background.fill_rect(1, 1, DEFAULT_MAP_SIZE, DEFAULT_MAP_SIZE)
+
+/datum/component/camera_manager/proc/update_target_camera()
+ // Show static if can't use the camera
+ if(!current?.can_use())
+ show_camera_static()
+ return
+
+ // Is this camera located in or attached to a living thing, Vehicle or helmet? If so, assume the camera's loc is the living (or non) thing.
+ var/cam_location = current
+ if(isliving(current.loc) || isVehicle(current.loc))
+ cam_location = current.loc
+ else if(istype(current.loc, /obj/item/clothing/head/helmet/marine))
+ var/obj/item/clothing/head/helmet/marine/helmet = current.loc
+ cam_location = helmet.loc
+
+ // If we're not forcing an update for some reason and the cameras are in the same location,
+ // we don't need to update anything.
+ // Most security cameras will end here as they're not moving.
+ var/newturf = get_turf(cam_location)
+ if(last_camera_turf == newturf)
+ return
+
+ // Cameras that get here are moving, and are likely attached to some moving atom such as cyborgs.
+ last_camera_turf = get_turf(cam_location)
+
+ var/list/visible_things = current.isXRay() ? range(current.view_range, cam_location) : view(current.view_range, cam_location)
+ render_objects(visible_things)
+
+/datum/component/camera_manager/proc/update_area_camera()
+ // Show static if can't use the camera
+ if(!current_area || !target_z)
+ show_camera_static()
+ return
+
+ // If we're not forcing an update for some reason and the cameras are in the same location,
+ // we don't need to update anything.
+ // Most security cameras will end here as they're not moving.
+ var/turf/new_location = locate(target_x, target_y, target_z)
+ if(last_camera_turf == new_location)
+ return
+
+ // Cameras that get here are moving, and are likely attached to some moving atom such as cyborgs.
+ last_camera_turf = new_location
+
+ var/x_size = current_area.width
+ var/y_size = current_area.height
+ var/turf/target = locate(current_area.center_x, current_area.center_y, target_z)
+
+ var/list/visible_things = isXRay ? range("[x_size]x[y_size]", target) : view("[x_size]x[y_size]", target)
+ render_objects(visible_things)
+
+/datum/component/camera_manager/proc/render_objects(list/visible_things)
+ var/list/visible_turfs = list()
+ for(var/turf/visible_turf in visible_things)
+ visible_turfs += visible_turf
+
+ var/list/bbox = get_bbox_of_atoms(visible_turfs)
+ var/size_x = bbox[3] - bbox[1] + 1
+ var/size_y = bbox[4] - bbox[2] + 1
+
+ cam_screen.vis_contents = visible_turfs
+ cam_background.icon_state = "clear"
+ cam_background.fill_rect(1, 1, size_x, size_y)
+
+#undef DEFAULT_MAP_SIZE
+#undef RENDER_MODE_TARGET
+#undef RENDER_MODE_AREA
diff --git a/code/game/cas_manager/datums/cas_fire_envelope.dm b/code/game/cas_manager/datums/cas_fire_envelope.dm
index 330521f34e..864d7f23a3 100644
--- a/code/game/cas_manager/datums/cas_fire_envelope.dm
+++ b/code/game/cas_manager/datums/cas_fire_envelope.dm
@@ -1,10 +1,12 @@
/datum/cas_fire_envelope
var/obj/structure/machinery/computer/dropship_weapons/linked_console
var/list/datum/cas_fire_mission/missions
- var/max_mission_len = 5
var/fire_length
var/grace_period //how much time you have after initiating fire mission and before you can't change firemissions
- var/flyto_period //how much time it takes from sound alarm start to first hit. CAS is vulnerable here
+ var/first_warning
+ var/second_warning
+ var/third_warning
+ var/execution_start
var/flyoff_period //how much time it takes after shots fired to get off the map. CAS is vulnerable here
var/cooldown_period //how much time you have to wait before new Fire Mission run
var/soundeffect //what sound effect to play
@@ -20,7 +22,7 @@
var/datum/cas_signal/recorded_loc = null
var/obj/effect/firemission_guidance/guidance
-
+ var/atom/tracked_object
/datum/cas_fire_envelope/New()
..()
@@ -28,37 +30,35 @@
/datum/cas_fire_envelope/Destroy(force, ...)
linked_console = null
+ untrack_object()
return ..()
-/datum/cas_fire_envelope/proc/get_total_duration()
- return grace_period+flyto_period+flyoff_period
+/datum/cas_fire_envelope/ui_data(mob/user)
+ . = list()
+ .["missions"] = list()
+ for(var/datum/cas_fire_mission/mission in missions)
+ .["missions"] += list(mission.ui_data(user))
+
+/datum/cas_fire_envelope/proc/update_weapons(list/obj/structure/dropship_equipment/weapon/weapons)
+ for(var/datum/cas_fire_mission/mission in missions)
+ mission.update_weapons(weapons, fire_length)
/datum/cas_fire_envelope/proc/generate_mission(firemission_name, length)
- if(!missions || !linked_console || missions.len>max_mission_len || !fire_length)
+ if(!missions || !linked_console || !fire_length)
return null
-
var/list/obj/structure/dropship_equipment/weapons = list()
- for(var/X in linked_console.shuttle_equipments)
- var/obj/structure/dropship_equipment/E = X
- if(E.is_weapon)
- weapons += E
+ var/shuttle_tag = linked_console.shuttle_tag
+ var/obj/docking_port/mobile/marine_dropship/dropship = SSshuttle.getShuttle(shuttle_tag)
+ for(var/obj/structure/dropship_equipment/equipment as anything in dropship.equipments)
+ if(equipment.is_weapon)
+ weapons += equipment
var/datum/cas_fire_mission/fm = new()
-
- if(weapons.len==0)
- return null //why bother?
-
for(var/obj/structure/dropship_equipment/weapon/wp in weapons)
- var/datum/cas_fire_mission_record/record = new()
- record.weapon = wp
- record.offsets = new /list(fire_length)
- for(var/idx = 1; idx<=fire_length; idx++)
- record.offsets[idx] = "-"
- fm.records += record
+ fm.build_new_record(wp, fire_length)
fm.name = firemission_name
fm.mission_length = length
-
missions += fm
return fm
@@ -67,67 +67,61 @@
mission_error = null
if(stat > FIRE_MISSION_STATE_IN_TRANSIT && stat < FIRE_MISSION_STATE_COOLDOWN)
mission_error = "Fire Mission is under way already."
- return 0
+ return FIRE_MISSION_NOT_EXECUTABLE
if(!missions[mission_id])
- return -1
+ return FIRE_MISSION_NOT_EXECUTABLE
var/datum/cas_fire_mission/mission = missions[mission_id]
if(!mission)
- return -1
- if(!mission.records[weapon_id])
- return -1
- var/datum/cas_fire_mission_record/fmr = mission.records[weapon_id]
+ return FIRE_MISSION_NOT_EXECUTABLE
+
+ var/datum/cas_fire_mission_record/fmr = mission.record_for_weapon(weapon_id)
+ if(!fmr)
+ return FIRE_MISSION_NOT_EXECUTABLE
if(!fmr.offsets || isnull(fmr.offsets[offset_step]))
- return -1
+ return FIRE_MISSION_NOT_EXECUTABLE
var/old_offset = fmr.offsets[offset_step]
+ if(offset == null)
+ offset = "-"
fmr.offsets[offset_step] = offset
var/check_result = mission.check(linked_console)
if(check_result == FIRE_MISSION_CODE_ERROR)
- return -1
+ return FIRE_MISSION_NOT_EXECUTABLE
if(check_result == FIRE_MISSION_ALL_GOOD)
- return 1
+ return FIRE_MISSION_ALL_GOOD
if(check_result == FIRE_MISSION_WEAPON_OUT_OF_AMMO)
- return 1
+ return FIRE_MISSION_ALL_GOOD
mission_error = mission.error_message(check_result)
if(skip_checks)
- return 0
+ return FIRE_MISSION_ALL_GOOD
//we have mission error. Fill the thing and restore previous state
fmr.offsets[offset_step] = old_offset
- return 0
+ return FIRE_MISSION_ALL_GOOD
-/datum/cas_fire_envelope/proc/execute_firemission(datum/cas_signal/target_turf, offset, dir, mission_id)
- if(!istype(target_turf))
- mission_error = "No target."
- return 0
+/datum/cas_fire_envelope/proc/execute_firemission(datum/cas_signal/signal, target_turf,dir, mission_id)
if(stat != FIRE_MISSION_STATE_IDLE)
mission_error = "Fire Mission is under way already."
- return 0
+ return FIRE_MISSION_NOT_EXECUTABLE
if(!missions[mission_id])
- return -1
- if(offset<0)
- mission_error = "Can't have negative offsets."
- return 0
- if(offset>max_offset)
- mission_error = "[max_offset] is the maximum possible offset."
- return 0
+ return FIRE_MISSION_NOT_EXECUTABLE
if(dir!=NORTH && dir!=SOUTH && dir!=WEST && dir!=EAST)
mission_error = "Incorrect direction."
- return 0
+ return FIRE_MISSION_BAD_DIRECTION
mission_error = null
var/datum/cas_fire_mission/mission = missions[mission_id]
var/check_result = mission.check(linked_console)
if(check_result == FIRE_MISSION_CODE_ERROR)
- return -1
+ return FIRE_MISSION_CODE_ERROR
if(check_result != FIRE_MISSION_ALL_GOOD)
mission_error = mission.error_message(check_result)
- return 0
+ return FIRE_MISSION_CODE_ERROR
//actual firemission code
- execute_firemission_unsafe(target_turf, offset, dir, mission)
- return 1
+ execute_firemission_unsafe(signal, target_turf, dir, mission)
+ return FIRE_MISSION_ALL_GOOD
/datum/cas_fire_envelope/proc/firemission_status_message()
switch(stat)
@@ -157,7 +151,9 @@
recorded_loc = marker
return TRUE
-/datum/cas_fire_envelope/proc/change_current_loc(location)
+/datum/cas_fire_envelope/proc/change_current_loc(location, atom/object)
+ if(object)
+ untrack_object()
if(!location && guidance)
for(var/mob/M in guidance.users)
if(istype(M) && M.client)
@@ -167,6 +163,22 @@
if(!guidance)
guidance = new /obj/effect/firemission_guidance()
guidance.forceMove(location)
+ guidance.updateCameras(linked_console)
+ if(object)
+ tracked_object = object
+ RegisterSignal(tracked_object, COMSIG_PARENT_QDELETING, PROC_REF(on_tracked_object_del))
+
+/// Call to unregister the on_tracked_object_del behavior
+/datum/cas_fire_envelope/proc/untrack_object()
+ if(tracked_object)
+ UnregisterSignal(tracked_object, COMSIG_PARENT_QDELETING)
+ tracked_object = null
+
+/// Signal handler for when we are viewing a object in cam is qdel'd (but camera actually is actually some other obj)
+/datum/cas_fire_envelope/proc/on_tracked_object_del(atom/target)
+ SIGNAL_HANDLER
+ tracked_object = null
+ change_current_loc()
/datum/cas_fire_envelope/proc/user_is_guided(user)
return guidance && (user in guidance.users)
@@ -183,7 +195,6 @@
guidance.users += user
RegisterSignal(user, COMSIG_MOB_RESISTED, PROC_REF(exit_cam_resist))
-
/datum/cas_fire_envelope/proc/apply_upgrade(user)
var/mob/M = user
if(linked_console.upgraded == MATRIX_NVG)
@@ -193,8 +204,6 @@
M.overlay_fullscreen("matrix", /atom/movable/screen/fullscreen/flash/noise/nvg)
M.lighting_alpha = LIGHTING_PLANE_ALPHA_MOSTLY_INVISIBLE
M.sync_lighting_plane_alpha()
- else if (linked_console.upgraded == MATRIX_WIDE)
- M.client?.change_view(linked_console.power + 5, M)
/datum/cas_fire_envelope/proc/remove_upgrades(user)
@@ -221,6 +230,7 @@
remove_upgrades(user)
guidance.users -= user
UnregisterSignal(user, COMSIG_MOB_RESISTED)
+ guidance.clearCameras(linked_console)
/datum/cas_fire_envelope/proc/exit_cam_resist(mob/user)
SIGNAL_HANDLER
@@ -231,54 +241,93 @@
/datum/cas_fire_envelope/proc/check_firemission_loc(datum/cas_signal/target_turf)
return TRUE //redefined in child class
-/datum/cas_fire_envelope/proc/execute_firemission_unsafe(datum/cas_signal/target_turf, offset, dir, datum/cas_fire_mission/mission)
- var/sx = 0
- var/sy = 0
+/// Step 1: Sets the stat to FIRE_MISSION_STATE_ON_TARGET and starts the sound effect for the fire mission.
+/datum/cas_fire_envelope/proc/play_sound(atom/target_turf)
+ stat = FIRE_MISSION_STATE_ON_TARGET
+ change_current_loc(target_turf)
+ playsound(target_turf, soundeffect, vol = 70, vary = TRUE, sound_range = 50, falloff = 8)
+
+/// Step 2, 3, 4: Warns nearby mobs of the incoming fire mission. Warning as 1 is non-precise, whereas 2 and 3 are precise.
+/datum/cas_fire_envelope/proc/chat_warning(atom/target_turf, range = 10, warning_number = 1)
+ var/ds_identifier = "LARGE BIRD"
+ var/fm_identifier = "SPIT FIRE"
+ var/relative_dir
+ for(var/mob/mob in range(15, target_turf))
+ if (mob.mob_flags & KNOWS_TECHNOLOGY)
+ ds_identifier = "DROPSHIP"
+ fm_identifier = "FIRE"
+ if(get_turf(mob) == target_turf)
+ relative_dir = 0
+ else
+ relative_dir = Get_Compass_Dir(mob, target_turf)
+ switch(warning_number)
+ if(1)
+ mob.show_message( \
+ SPAN_HIGHDANGER("YOU HEAR THE [ds_identifier] ROAR AS IT PREPARES TO [fm_identifier] NEAR YOU!"),SHOW_MESSAGE_VISIBLE, \
+ SPAN_HIGHDANGER("YOU HEAR SOMETHING FLYING CLOSER TO YOU!") , SHOW_MESSAGE_AUDIBLE \
+ )
+ if(2)
+ mob.show_message( \
+ SPAN_HIGHDANGER("A [ds_identifier] FLIES [SPAN_UNDERLINE(relative_dir ? uppertext(("TO YOUR " + dir2text(relative_dir))) : uppertext("right above you"))]!"), SHOW_MESSAGE_VISIBLE, \
+ SPAN_HIGHDANGER("YOU HEAR SOMETHING GO [SPAN_UNDERLINE(relative_dir ? uppertext(("TO YOUR " + dir2text(relative_dir))) : uppertext("right above you"))]!"), SHOW_MESSAGE_AUDIBLE \
+ )
+ if(3)
+ mob.show_message( \
+ SPAN_HIGHDANGER("A [ds_identifier] FLIES [SPAN_UNDERLINE(relative_dir ? uppertext(("TO YOUR " + dir2text(relative_dir))) : uppertext("right above you"))]!"), SHOW_MESSAGE_VISIBLE, \
+ SPAN_HIGHDANGER("YOU HEAR SOMETHING GO [SPAN_UNDERLINE(relative_dir ? uppertext(("TO YOUR " + dir2text(relative_dir))) : uppertext("right above you"))]!"), SHOW_MESSAGE_AUDIBLE \
+ )
+
+/// Step 5: Actually executes the fire mission updating stat to FIRE_MISSION_STATE_FIRING and then FIRE_MISSION_STATE_OFF_TARGET
+/datum/cas_fire_envelope/proc/open_fire(atom/target_turf,datum/cas_fire_mission/mission,dir)
+ stat = FIRE_MISSION_STATE_FIRING
+ mission.execute_firemission(linked_console, target_turf, dir, fire_length, step_delay, src)
+ stat = FIRE_MISSION_STATE_OFF_TARGET
- recorded_dir = dir
- recorded_offset = offset
+/// Step 6: Sets the fire mission stat to FIRE_MISSION_STATE_COOLDOWN
+/datum/cas_fire_envelope/proc/flyoff()
+ stat = FIRE_MISSION_STATE_COOLDOWN
+/// Step 7: Sets the fire mission stat to FIRE_MISSION_STATE_IDLE
+/datum/cas_fire_envelope/proc/end_cooldown()
+ stat = FIRE_MISSION_STATE_IDLE
+
+
+/datum/cas_fire_envelope/proc/execute_firemission_unsafe(datum/cas_signal/signal, turf/target_turf, dir, datum/cas_fire_mission/mission)
stat = FIRE_MISSION_STATE_IN_TRANSIT
- sleep(grace_period)
- stat = FIRE_MISSION_STATE_ON_TARGET
- switch(recorded_dir)
- if(NORTH) //default direction
- sx = 0
- sy = 1
- if(SOUTH)
- sx = 0
- sy = -1
- if(EAST)
- sx = 1
- sy = 0
- if(WEST)
- sx = -1
- sy = 0
+ to_chat(usr, SPAN_ALERT("Firemission underway!"))
if(!target_turf)
stat = FIRE_MISSION_STATE_IDLE
mission_error = "Target Lost."
return
- var/turf/tt_turf = get_turf(target_turf.signal_loc)
- if(!tt_turf || !check_firemission_loc(target_turf))
+ if(!target_turf || !check_firemission_loc(signal))
stat = FIRE_MISSION_STATE_IDLE
mission_error = "Target is off bounds or obstructed."
return
- var/turf/shootloc = locate(tt_turf.x + sx*recorded_offset, tt_turf.y + sy*recorded_offset,tt_turf.z)
- if(!shootloc || !istype(shootloc))
- stat = FIRE_MISSION_STATE_IDLE
- mission_error = "Target is off bounds."
- return
- change_current_loc(shootloc)
- playsound(shootloc, soundeffect, 70, TRUE, 50)
- sleep(flyto_period)
- stat = FIRE_MISSION_STATE_FIRING
- mission.execute_firemission(linked_console, shootloc, recorded_dir, fire_length, step_delay, src)
- stat = FIRE_MISSION_STATE_OFF_TARGET
- sleep(flyoff_period)
- stat = FIRE_MISSION_STATE_COOLDOWN
- sleep(cooldown_period)
- stat = FIRE_MISSION_STATE_IDLE
+ var/obj/effect/firemission_effect = new(target_turf)
+
+ firemission_effect.icon = 'icons/obj/items/weapons/projectiles.dmi'
+ firemission_effect.icon_state = "laser_target2"
+ firemission_effect.mouse_opacity = MOUSE_OPACITY_TRANSPARENT
+ firemission_effect.invisibility = INVISIBILITY_MAXIMUM
+ QDEL_IN(firemission_effect, 12 SECONDS)
+
+
+ notify_ghosts(header = "CAS Fire Mission", message = "[usr ? usr : "Someone"] is launching Fire Mission '[mission.name]' at [get_area(target_turf)].", source = firemission_effect)
+ msg_admin_niche("[usr ? key_name(usr) : "Someone"] is launching Fire Mission '[mission.name]' at ([target_turf.x],[target_turf.y],[target_turf.z]) [ADMIN_JMP(target_turf)]")
+
+
+ addtimer(CALLBACK(src, PROC_REF(play_sound), target_turf), grace_period)
+ addtimer(CALLBACK(src, PROC_REF(chat_warning), target_turf, 15, 1), first_warning)
+ addtimer(CALLBACK(src, PROC_REF(chat_warning), target_turf, 15, 2), second_warning)
+ addtimer(CALLBACK(src, PROC_REF(chat_warning), target_turf, 10, 3), third_warning)
+ addtimer(CALLBACK(src, PROC_REF(open_fire), target_turf, mission,dir), execution_start)
+ addtimer(CALLBACK(src, PROC_REF(flyoff)), flyoff_period)
+ addtimer(CALLBACK(src, PROC_REF(end_cooldown)), cooldown_period)
+
+/**
+ * Change attack vector for firemission
+ */
/datum/cas_fire_envelope/proc/change_direction(new_dir)
if(stat > FIRE_MISSION_STATE_IN_TRANSIT)
mission_error = "Fire Mission is under way already."
@@ -318,10 +367,13 @@
/datum/cas_fire_envelope/uscm_dropship
fire_length = 12
- grace_period = 50 //5 seconds
- flyto_period = 50 //five seconds
- flyoff_period = 50 //FIVE seconds
- cooldown_period = 100 //f~ I mean, 10 seconds
+ grace_period = 5 SECONDS
+ first_warning = 6 SECONDS
+ second_warning = 8 SECONDS
+ third_warning = 9 SECONDS
+ execution_start = 10 SECONDS
+ flyoff_period = 15 SECONDS
+ cooldown_period = 25 SECONDS
soundeffect = 'sound/weapons/dropship_sonic_boom.ogg' //BOOM~WOOOOOSH~HSOOOOOW~BOOM
step_delay = 3
max_offset = 12
@@ -346,13 +398,13 @@
/obj/structure/machinery/computer/dropship_weapons/proc/update_mission(mission_id, weapon_id, offset_step, offset)
var/result = firemission_envelope.update_mission(mission_id, weapon_id, offset_step, offset)
- if(result<1)
+ if(result != FIRE_MISSION_ALL_GOOD)
return firemission_envelope.mission_error
return "OK"
// Used in the simulation room for firemission testing.
/obj/structure/machinery/computer/dropship_weapons/proc/execute_firemission(obj/location, offset, dir, mission_id)
var/result = firemission_envelope.execute_firemission(get_turf(location), offset, dir, mission_id)
- if(result<1)
+ if(result != FIRE_MISSION_ALL_GOOD)
return firemission_envelope.mission_error
return "OK"
diff --git a/code/game/cas_manager/datums/cas_fire_mission.dm b/code/game/cas_manager/datums/cas_fire_mission.dm
index 0a04876414..dc55e057ed 100644
--- a/code/game/cas_manager/datums/cas_fire_mission.dm
+++ b/code/game/cas_manager/datums/cas_fire_mission.dm
@@ -1,17 +1,83 @@
/obj/effect/firemission_guidance
invisibility = 101
- var/list/users
+ var/list/mob/users
+ var/camera_width = 11
+ var/camera_height = 11
+ var/view_range = 7
/obj/effect/firemission_guidance/New()
..()
users = list()
+/obj/effect/firemission_guidance/Destroy(force)
+ . = ..()
+ users = null
+
+/obj/effect/firemission_guidance/proc/can_use()
+ return TRUE
+
+/obj/effect/firemission_guidance/proc/isXRay()
+ return FALSE
+
+/obj/effect/firemission_guidance/proc/updateCameras(atom/target)
+ SEND_SIGNAL(target, COMSIG_CAMERA_SET_TARGET, src, camera_width, camera_height)
+
+/obj/effect/firemission_guidance/proc/clearCameras(atom/target)
+ SEND_SIGNAL(target, COMSIG_CAMERA_CLEAR)
+
/datum/cas_fire_mission
var/mission_length = 3 //can be 3,4,6 or 12
var/list/datum/cas_fire_mission_record/records = list()
var/obj/structure/dropship_equipment/weapon/error_weapon
var/name = "Unnamed Firemission"
+/datum/cas_fire_mission/ui_data(mob/user)
+ . = list()
+ .["name"] = sanitize(copytext(name, 1, MAX_MESSAGE_LEN))
+ .["records"] = list()
+ for(var/datum/cas_fire_mission_record/record as anything in records)
+ .["records"] += list(record.ui_data(user))
+
+/datum/cas_fire_mission/proc/build_new_record(obj/structure/dropship_equipment/weapon/weapon, fire_length)
+ var/datum/cas_fire_mission_record/record = new()
+ record.weapon = weapon
+ record.offsets = new /list(fire_length)
+ for(var/idx = 1; idx<=fire_length; idx++)
+ record.offsets[idx] = "-"
+ records += record
+
+/datum/cas_fire_mission/proc/update_weapons(list/obj/structure/dropship_equipment/weapon/weapons, fire_length)
+ var/list/datum/cas_fire_mission_record/bad_records = list()
+ var/list/obj/structure/dropship_equipment/weapon/missing_weapons = list()
+ for(var/datum/cas_fire_mission_record/record in records)
+ // if weapon appears in weapons list but not in record
+ // > add empty record for new weapon
+ var/found = FALSE
+ for(var/obj/structure/dropship_equipment/weapon/weapon in weapons)
+ if(record.weapon == weapon)
+ found=TRUE
+ break
+ if(!found)
+ bad_records.Add(record)
+ for(var/obj/structure/dropship_equipment/weapon/weapon in weapons)
+ var/found = FALSE
+ for(var/datum/cas_fire_mission_record/record in records)
+ if(record.weapon == weapon)
+ found=TRUE
+ break
+ if(!found)
+ missing_weapons.Add(weapon)
+ for(var/datum/cas_fire_mission_record/record in bad_records)
+ records -= record
+ for(var/obj/structure/dropship_equipment/weapon/weapon in missing_weapons)
+ build_new_record(weapon, fire_length)
+
+/datum/cas_fire_mission/proc/record_for_weapon(weapon_id)
+ for(var/datum/cas_fire_mission_record/record as anything in records)
+ if(record.weapon.ship_base.attach_id == weapon_id)
+ return record
+ return null
+
/datum/cas_fire_mission/proc/check(obj/structure/machinery/computer/dropship_weapons/linked_console)
error_weapon = null
if(records.len == 0)
@@ -98,51 +164,6 @@
if(initial_turf == null || check(linked_console) != FIRE_MISSION_ALL_GOOD)
return FIRE_MISSION_NOT_EXECUTABLE
- var/obj/effect/firemission_effect = new(initial_turf)
-
- firemission_effect.icon = 'icons/obj/items/weapons/projectiles.dmi'
- firemission_effect.icon_state = "laser_target2"
- firemission_effect.mouse_opacity = MOUSE_OPACITY_TRANSPARENT
- firemission_effect.invisibility = INVISIBILITY_MAXIMUM
- QDEL_IN(firemission_effect, 5 SECONDS)
-
- notify_ghosts(header = "CAS Fire Mission", message = "[usr ? usr : "Someone"] is launching Fire Mission '[name]' at [get_area(initial_turf)].", source = firemission_effect)
- msg_admin_niche("[usr ? key_name(usr) : "Someone"] is launching Fire Mission '[name]' at ([initial_turf.x],[initial_turf.y],[initial_turf.z]) [ADMIN_JMP(initial_turf)]")
-
- var/relative_dir
- for(var/mob/M in range(15, initial_turf))
- if(get_turf(M) == initial_turf)
- relative_dir = 0
- else
- relative_dir = get_dir(M, initial_turf)
-
- var/ds_identifier = "LARGE BIRD"
- if (M.mob_flags & KNOWS_TECHNOLOGY)
- ds_identifier = "DROPSHIP"
-
- M.show_message( \
- SPAN_HIGHDANGER("A [ds_identifier] FLIES [SPAN_UNDERLINE(relative_dir ? uppertext(("TO YOUR " + dir2text(relative_dir))) : uppertext("right above you"))]!"), SHOW_MESSAGE_VISIBLE, \
- SPAN_HIGHDANGER("YOU HEAR SOMETHING GO [SPAN_UNDERLINE(relative_dir ? uppertext(("TO YOUR " + dir2text(relative_dir))) : uppertext("right above you"))]!"), SHOW_MESSAGE_AUDIBLE \
- )
-
- // Xenos have time to react to the first message
- sleep(0.5 SECONDS)
-
- for(var/mob/M in range(10, initial_turf))
- if(get_turf(M) == initial_turf)
- relative_dir = 0
- else
- relative_dir = get_dir(M, initial_turf)
-
- var/ds_identifier = "LARGE BIRD"
- if (M.mob_flags & KNOWS_TECHNOLOGY)
- ds_identifier = "DROPSHIP"
-
- M.show_message( \
- SPAN_HIGHDANGER("A [ds_identifier] FIRES [SPAN_UNDERLINE(relative_dir ? uppertext(("TO YOUR " + dir2text(relative_dir))) : uppertext("right above you"))]!"), 1, \
- SPAN_HIGHDANGER("YOU HEAR SOMETHING FIRE [SPAN_UNDERLINE(relative_dir ? uppertext(("TO YOUR " + dir2text(relative_dir))) : uppertext("right above you"))]!"), 2 \
- )
-
var/turf/current_turf = initial_turf
var/tally_step = steps / mission_length //how much shots we need before moving to next turf
var/next_step = tally_step //when we move to next turf
@@ -165,7 +186,7 @@
var/step = 1
for(step = 1; step<=steps; step++)
if(step > next_step)
- current_turf = get_step(current_turf,direction)
+ current_turf = get_step(current_turf, direction)
next_step += tally_step
if(envelope)
envelope.change_current_loc(current_turf)
@@ -177,8 +198,8 @@
if (current_turf == null)
return -1
var/turf/shootloc = locate(current_turf.x + sx*offset, current_turf.y + sy*offset, current_turf.z)
- var/area/A = get_area(shootloc)
- if(shootloc && !CEILING_IS_PROTECTED(A?.ceiling, CEILING_PROTECTION_TIER_3) && !protected_by_pylon(TURF_PROTECTION_CAS, shootloc))
+ var/area/area = get_area(shootloc)
+ if(shootloc && !CEILING_IS_PROTECTED(area?.ceiling, CEILING_PROTECTION_TIER_3) && !protected_by_pylon(TURF_PROTECTION_CAS, shootloc))
item.weapon.open_fire_firemission(shootloc)
sleep(step_delay)
if(envelope)
diff --git a/code/game/machinery/computer/camera_console.dm b/code/game/machinery/computer/camera_console.dm
index b78f306162..a1ea4f9f87 100644
--- a/code/game/machinery/computer/camera_console.dm
+++ b/code/game/machinery/computer/camera_console.dm
@@ -13,58 +13,35 @@
var/list/concurrent_users = list()
// Stuff needed to render the map
- var/map_name
- var/atom/movable/screen/map_view/cam_screen
- var/atom/movable/screen/background/cam_background
+ var/camera_map_name
var/colony_camera_mapload = TRUE
var/admin_console = FALSE
- /// All the plane masters that need to be applied.
- var/list/cam_plane_masters
-
/obj/structure/machinery/computer/cameras/Initialize(mapload)
. = ..()
- // Map name has to start and end with an A-Z character,
- // and definitely NOT with a square bracket or even a number.
- // I wasted 6 hours on this. :agony:
- map_name = "camera_console_[REF(src)]_map"
+
+ RegisterSignal(src, COMSIG_CAMERA_MAPNAME_ASSIGNED, PROC_REF(camera_mapname_update))
+
+ // camera setup
+ AddComponent(/datum/component/camera_manager)
+ SEND_SIGNAL(src, COMSIG_CAMERA_CLEAR)
if(colony_camera_mapload && mapload && is_ground_level(z))
network = list(CAMERA_NET_COLONY)
- cam_plane_masters = list()
- for(var/plane in subtypesof(/atom/movable/screen/plane_master) - /atom/movable/screen/plane_master/blackness)
- var/atom/movable/screen/plane_master/instance = new plane()
- instance.assigned_map = map_name
- instance.del_on_map_removal = FALSE
- if(instance.blend_mode_override)
- instance.blend_mode = instance.blend_mode_override
- instance.screen_loc = "[map_name]:CENTER"
- cam_plane_masters += instance
-
- // Initialize map objects
- cam_screen = new
- cam_screen.icon = null
- cam_screen.name = "screen"
- cam_screen.assigned_map = map_name
- cam_screen.del_on_map_removal = FALSE
- cam_screen.screen_loc = "[map_name]:1,1"
- cam_background = new
- cam_background.assigned_map = map_name
- cam_background.del_on_map_removal = FALSE
/obj/structure/machinery/computer/cameras/Destroy()
SStgui.close_uis(src)
QDEL_NULL(current)
- QDEL_NULL(cam_screen)
- qdel(cam_screen)
- QDEL_NULL(cam_background)
- qdel(cam_background)
+ UnregisterSignal(src, COMSIG_CAMERA_MAPNAME_ASSIGNED)
last_camera_turf = null
concurrent_users = null
return ..()
+/obj/structure/machinery/computer/cameras/proc/camera_mapname_update(source, value)
+ camera_map_name = value
+
/obj/structure/machinery/computer/cameras/attack_remote(mob/user as mob)
return attack_hand(user)
@@ -91,8 +68,7 @@
// Update UI
ui = SStgui.try_update_ui(user, src, ui)
- // Update the camera, showing static if necessary and updating data if the location has moved.
- update_active_camera_screen()
+ SEND_SIGNAL(src, COMSIG_CAMERA_REFRESH)
if(!ui)
var/user_ref = WEAKREF(user)
@@ -104,11 +80,9 @@
// Turn on the console
if(length(concurrent_users) == 1 && is_living)
update_use_power(USE_POWER_ACTIVE)
- // Register map objects
- user.client.register_map_obj(cam_screen)
- user.client.register_map_obj(cam_background)
- for(var/plane in cam_plane_masters)
- user.client.register_map_obj(plane)
+
+ SEND_SIGNAL(src, COMSIG_CAMERA_REGISTER_UI, user)
+
// Open UI
ui = new(user, src, "CameraConsole", name)
ui.open()
@@ -126,7 +100,7 @@
/obj/structure/machinery/computer/cameras/ui_static_data()
var/list/data = list()
- data["mapRef"] = map_name
+ data["mapRef"] = camera_map_name
var/list/cameras = get_available_cameras()
data["cameras"] = list()
for(var/i in cameras)
@@ -160,47 +134,10 @@
if(!selected_camera)
return TRUE
- update_active_camera_screen()
+ SEND_SIGNAL(src, COMSIG_CAMERA_SET_TARGET, selected_camera, selected_camera.view_range, selected_camera.view_range)
return TRUE
-/obj/structure/machinery/computer/cameras/proc/update_active_camera_screen()
- // Show static if can't use the camera
- if(!current?.can_use())
- show_camera_static()
- return
-
- // Is this camera located in or attached to a living thing, Vehicle or helmet? If so, assume the camera's loc is the living (or non) thing.
- var/cam_location = current
- if(isliving(current.loc) || isVehicle(current.loc))
- cam_location = current.loc
- else if(istype(current.loc, /obj/item/clothing/head/helmet/marine))
- var/obj/item/clothing/head/helmet/marine/helmet = current.loc
- cam_location = helmet.loc
-
- // If we're not forcing an update for some reason and the cameras are in the same location,
- // we don't need to update anything.
- // Most security cameras will end here as they're not moving.
- var/newturf = get_turf(cam_location)
- if(last_camera_turf == newturf)
- return
-
- // Cameras that get here are moving, and are likely attached to some moving atom such as cyborgs.
- last_camera_turf = get_turf(cam_location)
-
- var/list/visible_things = current.isXRay() ? range(current.view_range, cam_location) : view(current.view_range, cam_location)
-
- var/list/visible_turfs = list()
- for(var/turf/visible_turf in visible_things)
- visible_turfs += visible_turf
-
- var/list/bbox = get_bbox_of_atoms(visible_turfs)
- var/size_x = bbox[3] - bbox[1] + 1
- var/size_y = bbox[4] - bbox[2] + 1
-
- cam_screen.vis_contents = visible_turfs
- cam_background.icon_state = "clear"
- cam_background.fill_rect(1, 1, size_x, size_y)
/obj/structure/machinery/computer/cameras/ui_close(mob/user)
var/user_ref = WEAKREF(user)
@@ -208,21 +145,16 @@
// Living creature or not, we remove you anyway.
concurrent_users -= user_ref
// Unregister map objects
- user.client.clear_map(map_name)
+ SEND_SIGNAL(src, COMSIG_CAMERA_UNREGISTER_UI, user)
// Turn off the console
if(length(concurrent_users) == 0 && is_living)
current = null
+ SEND_SIGNAL(src, COMSIG_CAMERA_CLEAR)
last_camera_turf = null
if(use_power)
update_use_power(USE_POWER_IDLE)
user.unset_interaction()
-/obj/structure/machinery/computer/cameras/proc/show_camera_static()
- cam_screen.vis_contents.Cut()
- last_camera_turf = null
- cam_background.icon_state = "scanline2"
- cam_background.fill_rect(1, 1, DEFAULT_MAP_SIZE, DEFAULT_MAP_SIZE)
-
// Returns the list of cameras accessible from this computer
/obj/structure/machinery/computer/cameras/proc/get_available_cameras()
var/list/D = list()
diff --git a/code/game/machinery/computer/demo_sim.dm b/code/game/machinery/computer/demo_sim.dm
index 15261cfc8f..f633e8f351 100644
--- a/code/game/machinery/computer/demo_sim.dm
+++ b/code/game/machinery/computer/demo_sim.dm
@@ -55,7 +55,6 @@
var/list/data = list()
data["configuration"] = configuration
- data["looking"] = simulation.looking_at_simulation
data["dummy_mode"] = simulation.dummy_mode
data["worldtime"] = world.time
@@ -104,8 +103,7 @@
/obj/structure/machinery/computer/demo_sim/ui_close(mob/user)
. = ..()
- if(simulation.looking_at_simulation)
- simulation.stop_watching(user)
+ simulation.stop_watching(user)
// DEMOLITIONS TGUI SHIT END \\
diff --git a/code/game/machinery/computer/dropship_weapons.dm b/code/game/machinery/computer/dropship_weapons.dm
index 370461cff7..7aaf2dae49 100644
--- a/code/game/machinery/computer/dropship_weapons.dm
+++ b/code/game/machinery/computer/dropship_weapons.dm
@@ -12,7 +12,6 @@
var/shuttle_tag // Used to know which shuttle we're linked to.
var/obj/structure/dropship_equipment/selected_equipment //the currently selected equipment installed on the shuttle this console controls.
- var/list/shuttle_equipments = list() //list of the equipments on the shuttle this console controls
var/cavebreaker = FALSE //ignore caves and other restrictions?
var/datum/cas_fire_envelope/firemission_envelope
var/datum/cas_fire_mission/selected_firemission
@@ -26,170 +25,101 @@
var/datum/simulator/simulation
var/datum/cas_fire_mission/configuration
+ // groundside maps
+ var/datum/tacmap/tacmap
+ var/minimap_type = MINIMAP_FLAG_USCM
+
+ // Cameras
+ var/camera_target_id
+ var/camera_width = 11
+ var/camera_height = 11
+ var/camera_map_name
+ ///Tracks equipment with a camera that is deployed and we are viewing
+ var/obj/structure/dropship_equipment/camera_area_equipment = null
+
+ var/registered = FALSE
+
/obj/structure/machinery/computer/dropship_weapons/Initialize()
. = ..()
simulation = new()
- firemission_envelope = new /datum/cas_fire_envelope/uscm_dropship()
- req_one_access = list(ACCESS_MARINE_LEADER, ACCESS_MARINE_DROPSHIP, ACCESS_WY_FLIGHT)
+ tacmap = new(src, minimap_type)
+
+ RegisterSignal(src, COMSIG_CAMERA_MAPNAME_ASSIGNED, PROC_REF(camera_mapname_update))
+
+ // camera setup
+ AddComponent(/datum/component/camera_manager)
+ SEND_SIGNAL(src, COMSIG_CAMERA_CLEAR)
/obj/structure/machinery/computer/dropship_weapons/New()
..()
if(firemission_envelope)
firemission_envelope.linked_console = src
+/obj/structure/machinery/computer/dropship_weapons/proc/camera_mapname_update(source, value)
+ camera_map_name = value
+
+/obj/structure/machinery/computer/dropship_weapons/Destroy()
+ . = ..()
+ UnregisterSignal(src, COMSIG_CAMERA_MAPNAME_ASSIGNED)
+
/obj/structure/machinery/computer/dropship_weapons/attack_hand(mob/user)
if(..())
return
- if(!allowed(user))
+ if(!allowed(user))
+ // TODO: Restore cas simulator
+ to_chat(user, SPAN_WARNING("Weapons modification access denied."))
+ return TRUE
// everyone can access the simulator, requested feature.
- to_chat(user, SPAN_WARNING("Weapons modification access denied, attempting to launch simulation."))
+ /*to_chat(user, SPAN_WARNING("Weapons modification access denied, attempting to launch simulation."))
if(!selected_firemission)
- to_chat(usr, SPAN_WARNING("Firemission must be selected before attempting to run the simulation"))
- return
+ to_chat(user, SPAN_WARNING("Firemission must be selected before attempting to run the simulation"))
+ return TRUE
tgui_interact(user)
- return 1
+ return FALSE*/
user.set_interaction(src)
ui_interact(user)
/obj/structure/machinery/computer/dropship_weapons/attackby(obj/item/W, mob/user as mob)
if(istype(W, /obj/item/frame/matrix_frame))
- var/obj/item/frame/matrix_frame/MATRIX = W
- if(MATRIX.state == ASSEMBLY_LOCKED)
+ var/obj/item/frame/matrix_frame/matrix = W
+ if(matrix.state == ASSEMBLY_LOCKED)
user.drop_held_item(W, src)
W.forceMove(src)
to_chat(user, SPAN_NOTICE("You swap the matrix in the dropship guidance camera system, destroying the older part in the process"))
- upgraded = MATRIX.upgrade
- matrixcol = MATRIX.matrixcol
- power = MATRIX.power
+ upgraded = matrix.upgrade
+ matrixcol = matrix.matrixcol
+ power = matrix.power
else
- to_chat(user, SPAN_WARNING("matrix is not complete!"))
+ to_chat(user, SPAN_WARNING("Matrix is not complete!"))
+
+/obj/structure/machinery/computer/dropship_weapons/proc/equipment_update(obj/docking_port/mobile/marine_dropship/dropship)
+ SIGNAL_HANDLER
+ var/list/obj/structure/dropship_equipment/weapons = list()
+ for(var/obj/structure/dropship_equipment/weapon/weap as anything in dropship.equipments)
+ weapons.Add(weap)
+ firemission_envelope.update_weapons(weapons)
/obj/structure/machinery/computer/dropship_weapons/ui_interact(mob/user, ui_key = "main", datum/nanoui/ui = null, force_open = 0)
- var/data[0]
var/obj/docking_port/mobile/marine_dropship/dropship = SSshuttle.getShuttle(shuttle_tag)
- if (!istype(dropship))
+ if(!istype(dropship))
return
- var/shuttle_state
- switch(dropship.mode)
- if(SHUTTLE_IDLE)
- shuttle_state = "idle"
- if(SHUTTLE_IGNITING)
- shuttle_state = "warmup"
- if(SHUTTLE_CALL)
- shuttle_state = "in_transit"
- if(SHUTTLE_CRASHED)
- shuttle_state = "crashed"
-
-
- var/list/equipment_data = list()
- var/list/targets_data = list()
- var/list/firemission_data = list()
- var/list/firemission_edit_data = list()
- var/list/firemission_edit_timeslices = list()
-
- for(var/ts = 1; ts<=firemission_envelope.fire_length; ts++)
- firemission_edit_timeslices += ts
-
- var/current_mission_error = null
- if(!faction)
- return //no faction, no weapons
-
- var/datum/cas_iff_group/cas_group = cas_groups[faction]
-
- if(!cas_group)
- return //broken group. No fighting
-
- for(var/X in cas_group.cas_signals)
- var/datum/cas_signal/LT = X
- if(!istype(LT) || !LT.valid_signal())
- continue
- var/area/laser_area = get_area(LT.signal_loc)
- targets_data += list(list("target_name" = "[LT.name] ([laser_area.name])", "target_tag" = LT.target_id))
- shuttle_equipments = dropship.equipments
- var/element_nbr = 1
- for(var/X in dropship.equipments)
- var/obj/structure/dropship_equipment/E = X
- equipment_data += list(list("name"= sanitize(copytext(E.name,1,MAX_MESSAGE_LEN)), "eqp_tag" = element_nbr, "is_weapon" = E.is_weapon, "is_interactable" = E.is_interactable))
- element_nbr++
- E.linked_console = src
-
-
- var/selected_eqp_name = ""
- var/selected_eqp_ammo_name = ""
- var/selected_eqp_ammo_amt = 0
- var/selected_eqp_max_ammo_amt = 0
var/screen_mode = 0
- var/fm_length = 0
- var/fm_offset = 0
- var/fm_direction = ""
- var/fm_step_text = ""
- var/firemission_signal
- var/firemission_stat = 0
- if(selected_equipment)
- selected_eqp_name = sanitize(copytext(selected_equipment.name,1,MAX_MESSAGE_LEN))
- if(selected_equipment.ammo_equipped)
- selected_eqp_ammo_name = sanitize(copytext(selected_equipment.ammo_equipped.name,1,MAX_MESSAGE_LEN))
- selected_eqp_ammo_amt = selected_equipment.ammo_equipped.ammo_count
- selected_eqp_max_ammo_amt = selected_equipment.ammo_equipped.max_ammo_count
- screen_mode = selected_equipment.screen_mode
-
- var/firemission_id = 1
- var/found_selected = FALSE
if(firemission_envelope)
- firemission_stat = firemission_envelope.stat
- fm_step_text = firemission_envelope.firemission_status_message()
- for(var/datum/cas_fire_mission/X in firemission_envelope.missions)
- if(!istype(X))
- continue //the fuck
- var/error_code = X.check(src)
-
- var/selected = X == selected_firemission
- if(error_code != FIRE_MISSION_ALL_GOOD && selected)
- selected = FALSE
- selected_firemission = null
- var/can_edit = error_code != FIRE_MISSION_CODE_ERROR && !selected
-
- if(selected)
- found_selected = TRUE
- var/can_interact = firemission_envelope.stat == FIRE_MISSION_STATE_IDLE && error_code == FIRE_MISSION_ALL_GOOD
- firemission_data += list(list("name"= sanitize(copytext(X.name,1,MAX_MESSAGE_LEN)), "mission_tag" = firemission_id, "can_edit" = can_edit, "can_interact" = can_interact, "selected" = selected))
- firemission_id++
-
if(!istype(editing_firemission))
editing_firemission = null
- //the fuck
if(editing_firemission)
var/error_code = editing_firemission.check(src)
var/can_edit = error_code != FIRE_MISSION_CODE_ERROR
- if(error_code != FIRE_MISSION_ALL_GOOD)
- current_mission_error = editing_firemission.error_message(error_code)
- else
- current_mission_error = null
if(!can_edit)
editing_firemission = null
//abort
- else
- screen_mode = 2
- for(var/datum/cas_fire_mission_record/firerec in editing_firemission.records)
- var/gimbal = firerec.get_offsets()
- var/ammo = firerec.get_ammo()
- var/offsets = new /list(firerec.offsets.len)
- for(var/idx = 1; idx < firerec.offsets.len; idx++)
- offsets[idx] = firerec.offsets[idx] == null ? "-" : firerec.offsets[idx]
- firemission_edit_data += list(list("name" = sanitize(copytext(firerec.weapon.name, 1, 50)), "ammo" = ammo, "gimbal" = gimbal, "offsets" = firerec.offsets))
-
- if(!found_selected)
- selected_firemission = null
-
- if(editing_firemission)
- fm_length = editing_firemission.mission_length
if((screen_mode != 0 && in_firemission_mode) || !selected_firemission)
in_firemission_mode = FALSE
@@ -199,497 +129,724 @@
selected_firemission = null
if(selected_firemission && in_firemission_mode)
screen_mode = 3
- fm_offset = firemission_envelope.recorded_offset
- fm_direction = dir2text(firemission_envelope.recorded_dir)
if(firemission_envelope.recorded_loc && (!firemission_envelope.recorded_loc.signal_loc || !firemission_envelope.recorded_loc.signal_loc:loc))
firemission_envelope.recorded_loc = null
- firemission_signal = firemission_envelope.recorded_loc?firemission_envelope.recorded_loc.get_name() : "NOT SELECTED"
- if(!fm_direction)
- fm_direction = "NOT SELECTED"
-
- if(screen_mode != 3 || !selected_firemission || shuttle_state != "in_transit")
- update_location(null)
- // /if(firemission_envelope)
-
- data = list(
- "shuttle_state" = shuttle_state,
- "fire_mission_enabled" = dropship.in_flyby,
- "equipment_data" = equipment_data,
- "targets_data" = targets_data,
- "selected_eqp" = selected_eqp_name,
- "selected_eqp_ammo_name" = selected_eqp_ammo_name,
- "selected_eqp_ammo_amt" = selected_eqp_ammo_amt,
- "selected_eqp_max_ammo_amt" = selected_eqp_max_ammo_amt,
- "screen_mode" = screen_mode,
- "firemission_data" = firemission_data,
- "editing_firemission" = editing_firemission,
- "editing_firemission_length" = fm_length,
- "firemission_edit_data" = firemission_edit_data,
- "current_mission_error" = current_mission_error,
- "firemission_edit_timeslices" = firemission_edit_timeslices,
- "has_firemission" = !!firemission_envelope,
- "can_firemission" = !!selected_firemission && shuttle_state == "in_transit",
- "can_launch_firemission" = !!selected_firemission && shuttle_state == "in_transit" && firemission_stat != FIRE_MISSION_STATE_IDLE,
- //firemission related stuff
- "firemission_name" = (selected_firemission ? selected_firemission.name : ""),
- "firemission_selected_laser" = firemission_signal,
- "firemission_offset" = fm_offset,
- "firemission_direction" = fm_direction,
- "firemission_message" = fm_step_text,
- "firemission_step" = firemission_stat,
- )
+ if(screen_mode != 3 || !selected_firemission || dropship.mode != SHUTTLE_CALL)
+ update_location(user, null)
+
+ tgui_interact(user)
+
+/obj/structure/machinery/computer/dropship_weapons/tgui_interact(mob/user, datum/tgui/ui)
+ if(!registered)
+ var/obj/docking_port/mobile/marine_dropship/dropship = SSshuttle.getShuttle(shuttle_tag)
+ RegisterSignal(dropship, COMSIG_DROPSHIP_ADD_EQUIPMENT, PROC_REF(equipment_update))
+ RegisterSignal(dropship, COMSIG_DROPSHIP_REMOVE_EQUIPMENT, PROC_REF(equipment_update))
+ registered = TRUE
- ui = nanomanager.try_update_ui(user, src, ui_key, ui, data, force_open)
+ if(!tacmap.map_holder)
+ var/level = SSmapping.levels_by_trait(tacmap.targeted_ztrait)
+ tacmap.map_holder = SSminimaps.fetch_tacmap_datum(level[1], tacmap.allowed_flags)
- if (!ui)
- ui = new(user, src, ui_key, "dropship_weapons_console.tmpl", "Weapons Control", 800, 600)
- ui.set_initial_data(data)
+ ui = SStgui.try_update_ui(user, src, ui)
+ if(!ui)
+ user.client.register_map_obj(tacmap.map_holder.map)
+ SEND_SIGNAL(src, COMSIG_CAMERA_REGISTER_UI, user)
+ ui = new(user, src, "DropshipWeaponsConsole", "Weapons Console")
ui.open()
- ui.set_auto_update(1)
-/obj/structure/machinery/computer/dropship_weapons/Topic(href, href_list)
- if(..())
- return
+/obj/structure/machinery/computer/dropship_weapons/ui_close(mob/user)
+ . = ..()
+ SEND_SIGNAL(src, COMSIG_CAMERA_UNREGISTER_UI, user)
+ simulation.stop_watching(user)
+
+/obj/structure/machinery/computer/dropship_weapons/ui_status(mob/user, datum/ui_state/state)
+ . = ..()
+ if(inoperable())
+ return UI_CLOSE
+ if(!faction)
+ return UI_CLOSE
+
+ var/datum/cas_iff_group/cas_group = cas_groups[faction]
+ if(!cas_group)
+ return UI_CLOSE
+
+/obj/structure/machinery/computer/dropship_weapons/ui_state(mob/user)
+ return GLOB.not_incapacitated_and_adjacent_strict_state
- add_fingerprint(usr)
+/obj/structure/machinery/computer/dropship_weapons/ui_static_data(mob/user)
+ . = list()
+ .["tactical_map_ref"] = tacmap.map_holder.map_ref
+ .["camera_map_ref"] = camera_map_name
+/obj/structure/machinery/computer/dropship_weapons/ui_data(mob/user)
+ . = list()
var/obj/docking_port/mobile/marine_dropship/dropship = SSshuttle.getShuttle(shuttle_tag)
if (!istype(dropship))
return
- if(href_list["equip_interact"])
- var/base_tag = text2num(href_list["equip_interact"])
- var/obj/structure/dropship_equipment/E = shuttle_equipments[base_tag]
- E.linked_console = src
- E.equipment_interact(usr)
-
- if(href_list["open_fire"])
- var/targ_id = text2num(href_list["open_fire"])
- var/mob/M = usr
- if(ishuman(M))
- var/mob/living/carbon/human/H = M
- if(!H.allow_gun_usage)
- to_chat(H, SPAN_WARNING("Your programming prevents you from operating dropship weaponry!"))
- return
- var/obj/structure/dropship_equipment/weapon/DEW = selected_equipment
- if(!selected_equipment || !selected_equipment.is_weapon)
- to_chat(usr, SPAN_WARNING("No weapon selected."))
- return
- if(!skillcheck(M, SKILL_PILOT, DEW.skill_required)) //only pilots can fire dropship weapons.
- to_chat(usr, SPAN_WARNING("You don't have the training to fire this weapon!"))
- return
-
- if(!faction)
- return //no faction, no weapons
-
- var/datum/cas_iff_group/cas_group = cas_groups[faction]
-
- if(!cas_group)
- return //broken group. No fighting
-
- for(var/X in cas_group.cas_signals)
- var/datum/cas_signal/LT = X
- if(LT.target_id == targ_id && LT.valid_signal())
- if(dropship.mode != SHUTTLE_CALL)
- to_chat(usr, SPAN_WARNING("Dropship can only fire while in flight."))
- return
- if(dropship.door_override)
- return
- if(!selected_equipment || !selected_equipment.is_weapon)
- to_chat(usr, SPAN_WARNING("No weapon selected."))
- return
- DEW = selected_equipment // for if the weapon somehow changes
- if(!skillcheck(M, SKILL_PILOT, DEW.skill_required)) //only pilots can fire dropship weapons.
- to_chat(usr, SPAN_WARNING("You don't have the training to fire this weapon!"))
- return
- if(!dropship.in_flyby && DEW.fire_mission_only)
- to_chat(usr, SPAN_WARNING("[DEW] requires a fire mission flight type to be fired."))
- return
-
- if(!DEW.ammo_equipped || DEW.ammo_equipped.ammo_count <= 0)
- to_chat(usr, SPAN_WARNING("[DEW] has no ammo."))
- return
- if(DEW.last_fired > world.time - DEW.firing_delay)
- to_chat(usr, SPAN_WARNING("[DEW] just fired, wait for it to cool down."))
- return
- if(!LT.signal_loc)
- return
- var/turf/TU = get_turf(LT.signal_loc)
- var/area/targ_area = get_area(LT.signal_loc)
- var/is_outside = FALSE
- if(is_ground_level(TU.z))
- switch(targ_area.ceiling)
- if(CEILING_NONE)
- is_outside = TRUE
- if(CEILING_GLASS)
- is_outside = TRUE
- if(!is_outside && !cavebreaker) //cavebreaker doesn't care
- to_chat(usr, SPAN_WARNING("INVALID TARGET: target must be visible from high altitude."))
- return
- if (protected_by_pylon(TURF_PROTECTION_CAS, TU))
- to_chat(usr, SPAN_WARNING("INVALID TARGET: biological-pattern interference with signal."))
- return
- if(!DEW.ammo_equipped.can_fire_at(TU, usr))
- return
-
- DEW.open_fire(LT.signal_loc)
- break
-
- if(href_list["deselect"])
- var/mob/M = usr
- if(!skillcheck(M, SKILL_PILOT, SKILL_PILOT_TRAINED)) //only pilots can fire dropship weapons.
- to_chat(usr, SPAN_WARNING("A screen with graphics and walls of physics and engineering values open, you immediately force it closed."))
- return
- selected_equipment = null
-
- if(href_list["create_mission"])
- var/mob/M = usr
- if(!skillcheck(M, SKILL_PILOT, SKILL_PILOT_TRAINED)) //only pilots can fire dropship weapons.
- to_chat(usr, SPAN_WARNING("A screen with graphics and walls of physics and engineering values open, you immediately force it closed."))
- return
- if(firemission_envelope.max_mission_len <= firemission_envelope.missions.len)
- to_chat(usr, SPAN_WARNING("Cannot store more than [firemission_envelope.max_mission_len] Fire Missions."))
- return
- var/fm_name = stripped_input(usr, "", "Enter Fire Mission Name", "Fire Mission [firemission_envelope.missions.len+1]", 50)
- if(!fm_name || length(fm_name) < 5)
- to_chat(usr, SPAN_WARNING("Name too short (at least 5 symbols)."))
- return
- var/fm_length = stripped_input(usr, "Enter length of the Fire Mission. Has to be less than [firemission_envelope.fire_length]. Use something that divides [firemission_envelope.fire_length] for optimal performance.", "Fire Mission Length (in tiles)", "[firemission_envelope.fire_length]", 5)
- var/fm_length_n = text2num(fm_length)
- if(!fm_length_n)
- to_chat(usr, SPAN_WARNING("Incorrect input format."))
- return
- if(fm_length_n > firemission_envelope.fire_length)
- to_chat(usr, SPAN_WARNING("Fire Mission is longer than allowed by this vehicle."))
- return
- if(firemission_envelope.stat != FIRE_MISSION_STATE_IDLE)
- to_chat(usr, SPAN_WARNING("Vehicle has to be idle to allow Fire Mission editing and creation."))
- return
- //everything seems to be fine now
- firemission_envelope.generate_mission(fm_name, fm_length_n)
-
- if(href_list["mission_tag_delete"])
- var/ref = text2num(href_list["mission_tag_delete"])
- var/mob/M = usr
- if(!skillcheck(M, SKILL_PILOT, SKILL_PILOT_TRAINED)) //only pilots can fire dropship weapons.
- to_chat(usr, SPAN_WARNING("A screen with graphics and walls of physics and engineering values open, you immediately force it closed."))
- return
- if(ref>firemission_envelope.missions.len)
- to_chat(usr, SPAN_WARNING("Fire Mission ID corrupted or already deleted."))
- return
- if(selected_firemission == firemission_envelope.missions[ref])
- to_chat(usr, SPAN_WARNING("Can't delete selected Fire Mission."))
- return
- var/result = firemission_envelope.delete_firemission(ref)
- if(result != 1)
- to_chat(usr, SPAN_WARNING("Unable to delete Fire Mission while in combat."))
- return
-
- if(href_list["mission_tag"])
- var/ref = text2num(href_list["mission_tag"])
- var/mob/M = usr
- if(!skillcheck(M, SKILL_PILOT, SKILL_PILOT_TRAINED)) //only pilots can fire dropship weapons.
- to_chat(usr, SPAN_WARNING("A screen with graphics and walls of physics and engineering values open, you immediately force it closed."))
- return
- if(ref>firemission_envelope.missions.len)
- to_chat(usr, SPAN_WARNING("Fire Mission ID corrupted or deleted."))
- return
- if(firemission_envelope.stat > FIRE_MISSION_STATE_IN_TRANSIT && firemission_envelope.stat < FIRE_MISSION_STATE_COOLDOWN)
- to_chat(usr, SPAN_WARNING("Fire Mission already underway."))
- return
- if(selected_firemission == firemission_envelope.missions[ref])
- selected_firemission = null
- else
- selected_firemission = firemission_envelope.missions[ref]
-
- if(href_list["mission_tag_edit"])
- var/ref = text2num(href_list["mission_tag_edit"])
- var/mob/M = usr
- if(!skillcheck(M, SKILL_PILOT, SKILL_PILOT_TRAINED)) //only pilots can fire dropship weapons.
- to_chat(usr, SPAN_WARNING("A screen with graphics and walls of physics and engineering values open, you immediately force it closed."))
- return
- if(ref>firemission_envelope.missions.len)
- to_chat(usr, SPAN_WARNING("Fire Mission ID corrupted or deleted."))
- return
- if(selected_firemission == firemission_envelope.missions[ref])
- to_chat(usr, SPAN_WARNING("Can't edit selected Fire Mission."))
- return
- if(firemission_envelope.stat > FIRE_MISSION_STATE_IN_TRANSIT && firemission_envelope.stat < FIRE_MISSION_STATE_COOLDOWN)
- to_chat(usr, SPAN_WARNING("Fire Mission already underway."))
- return
- editing_firemission = firemission_envelope.missions[ref]
-
- if(href_list["leave_firemission_editing"])
- editing_firemission = null
-
- if(href_list["switch_to_firemission"])
- var/mob/M = usr
- if(!skillcheck(M, SKILL_PILOT, SKILL_PILOT_TRAINED)) //only pilots can fire dropship weapons.
- to_chat(usr, SPAN_WARNING("A screen with graphics and walls of physics and engineering values open, you immediately force it closed."))
- return
- in_firemission_mode = TRUE
-
- if(href_list["switch_to_simulation"])
- if(!selected_firemission)
- to_chat(usr, SPAN_WARNING("Select a firemission before attempting to run the simulation"))
- return
-
- configuration = selected_firemission
-
- // simulation mode
- tgui_interact(usr)
-
- if(href_list["leave_firemission_execution"])
- var/mob/M = usr
- if(!skillcheck(M, SKILL_PILOT, SKILL_PILOT_TRAINED)) //only pilots can fire dropship weapons.
- to_chat(usr, SPAN_WARNING("A screen with graphics and walls of physics and engineering values open, you immediately force it closed."))
- return
- firemission_envelope.remove_user_from_tracking(usr)
- in_firemission_mode = FALSE
-
- if(href_list["change_direction"])
- var/mob/M = usr
- if(!skillcheck(M, SKILL_PILOT, SKILL_PILOT_TRAINED)) //only pilots can fire dropship weapons.
- to_chat(usr, SPAN_WARNING("A screen with graphics and walls of physics and engineering values open, you immediately force it closed."))
- return
- var/list/directions = list(dir2text(NORTH), dir2text(SOUTH), dir2text(EAST), dir2text(WEST))
- var/chosen = tgui_input_list(usr, "Select new Direction for the strafing run", "Select Direction", directions)
-
- var/chosen_dir = text2dir(chosen)
- if(!chosen_dir)
- to_chat(usr, SPAN_WARNING("Error with direction detected."))
- return
-
- update_direction(chosen_dir)
-
- if(href_list["change_offset"])
- var/mob/M = usr
- if(!skillcheck(M, SKILL_PILOT, SKILL_PILOT_TRAINED)) //only pilots can fire dropship weapons.
- to_chat(usr, SPAN_WARNING("A screen with graphics and walls of physics and engineering values open, you immediately force it closed."))
- return
-
- var/chosen = stripped_input(usr, "Select Fire Mission length, from 0 to [firemission_envelope.max_offset]", "Select Offset", "[firemission_envelope.recorded_offset]", 2)
- var/chosen_offset = text2num(chosen)
-
- if(chosen_offset == null)
- to_chat(usr, SPAN_WARNING("Error with offset detected."))
- return
-
- update_offset(chosen_offset)
-
- if(href_list["select_laser_firemission"])
- var/mob/M = usr
- var/targ_id = text2num(href_list["select_laser_firemission"])
- if(!targ_id)
- to_chat(usr, SPAN_WARNING("Bad Target."))
- return
- if(!skillcheck(M, SKILL_PILOT, SKILL_PILOT_TRAINED)) //only pilots can fire dropship weapons.
- to_chat(usr, SPAN_WARNING("A screen with graphics and walls of physics and engineering values open, you immediately force it closed."))
- return
- if(firemission_envelope.stat > FIRE_MISSION_STATE_IN_TRANSIT && firemission_envelope.stat < FIRE_MISSION_STATE_COOLDOWN)
- to_chat(usr, SPAN_WARNING("Fire Mission already underway."))
- return
- if(dropship.mode != SHUTTLE_CALL)
- to_chat(usr, SPAN_WARNING("Shuttle has to be in orbit."))
- return
- var/datum/cas_iff_group/cas_group = cas_groups[faction]
- var/datum/cas_signal/cas_sig
- for(var/X in cas_group.cas_signals)
- var/datum/cas_signal/LT = X
- if(LT.target_id == targ_id && LT.valid_signal())
- cas_sig = LT
- if(!cas_sig)
- to_chat(usr, SPAN_WARNING("Target lost or obstructed."))
- return
-
- update_location(cas_sig)
-
- if(href_list["execute_firemission"])
- var/mob/M = usr
- if(ishuman(M))
- var/mob/living/carbon/human/H = M
- if(!H.allow_gun_usage)
- to_chat(H, SPAN_WARNING("Your programming prevents you from operating dropship weaponry!"))
+ var/datum/cas_signal/sig = get_cas_signal(camera_target_id)
+ if(camera_target_id && !sig)
+ set_camera_target(null)
+
+ .["screen_mode"] = get_screen_mode()
+
+ // dropship info
+ .["shuttle_state"] = dropship.mode
+ .["fire_mission_enabled"] = dropship.in_flyby
+
+ // equipment info
+ .["equipment_data"] = get_sanitised_equipment(user, dropship)
+
+ // medevac targets
+ .["medevac_targets"] = list()
+ for(var/obj/structure/dropship_equipment/equipment as anything in dropship.equipments)
+ if (istype(equipment, /obj/structure/dropship_equipment/medevac_system))
+ var/obj/structure/dropship_equipment/medevac_system/medevac = equipment
+ .["medevac_targets"] += medevac.ui_data(user)
+ // fultons
+
+ .["fulton_targets"] = list()
+ for(var/obj/structure/dropship_equipment/equipment as anything in dropship.equipments)
+ if (istype(equipment, /obj/structure/dropship_equipment/fulton_system))
+ var/obj/structure/dropship_equipment/fulton_system/fult = equipment
+ .["fulton_targets"] += fult.ui_data(user)
+
+ .["targets_data"] = get_targets()
+ .["camera_target"] = camera_target_id
+
+ if(selected_equipment)
+ .["selected_eqp"] = selected_equipment.ship_base.attach_id
+ if(selected_equipment.ammo_equipped)
+ var/obj/structure/ship_ammo/ammo_equipped = selected_equipment.ammo_equipped
+ .["selected_eqp_ammo_name"] = sanitize(copytext(ammo_equipped.name, 1, MAX_MESSAGE_LEN))
+ .["selected_eqp_ammo_amt"] = ammo_equipped.ammo_count
+ .["selected_eqp_max_ammo_amt"] = ammo_equipped.max_ammo_count
+
+ // firemission info
+ .["has_firemission"] = !!firemission_envelope
+ .["can_firemission"] = !!selected_firemission && dropship.mode == SHUTTLE_CALL
+ if(editing_firemission)
+ .["editing_firemission"] = editing_firemission
+ .["editing_firemission_length"] = editing_firemission ? editing_firemission.mission_length : 0
+ var/error_code = editing_firemission.check(src)
+ .["current_mission_error"] = error_code != FIRE_MISSION_ALL_GOOD ? editing_firemission.error_message(error_code) : null
+ .["firemission_edit_data"] = get_edit_firemission_data()
+
+ if(firemission_envelope)
+ .["can_launch_firemission"] = !!selected_firemission && dropship.mode == SHUTTLE_CALL && firemission_envelope.stat != FIRE_MISSION_STATE_IDLE
+ .["firemission_data"] = get_firemission_data(user)
+ .["firemission_state"] = firemission_envelope.stat
+ .["firemission_offset"] = firemission_envelope.recorded_offset
+ .["firemission_message"] = firemission_envelope.firemission_status_message()
+ .["firemission_name"] = selected_firemission ? selected_firemission.name : ""
+ .["firemission_step"] = firemission_envelope.stat
+ .["firemission_selected_laser"] = firemission_envelope.recorded_loc ? firemission_envelope.recorded_loc.get_name() : "NOT SELECTED"
+
+ .["configuration"] = configuration
+ .["dummy_mode"] = simulation.dummy_mode
+ .["worldtime"] = world.time
+ .["nextdetonationtime"] = simulation.detonation_cooldown
+ .["detonation_cooldown"] = simulation.detonation_cooldown_time
+
+/obj/structure/machinery/computer/dropship_weapons/ui_act(action, list/params, datum/tgui/ui, datum/ui_state/state)
+ . = ..()
+ if(.)
+ return
+ var/obj/docking_port/mobile/marine_dropship/shuttle = SSshuttle.getShuttle(shuttle_tag)
+ if(shuttle.is_hijacked)
+ return
+
+ var/mob/user = ui.user
+ switch(action)
+ if("button_push")
+ playsound(src, get_sfx("terminal_button"), 25, FALSE)
+ return FALSE
+
+ if("select_equipment")
+ var/base_tag = params["equipment_id"]
+ ui_equip_interact(user, base_tag)
+ return TRUE
+
+ if("start_watching")
+ simulation.start_watching(user)
+ . = TRUE
+
+ if("stop_watching")
+ simulation.stop_watching(user)
+ . = TRUE
+
+ if("execute_simulated_firemission")
+ if(!configuration)
+ to_chat(user, SPAN_WARNING("No configured firemission"))
return
- if(!skillcheck(M, SKILL_PILOT, SKILL_PILOT_TRAINED)) //only pilots can fire dropship weapons.
- to_chat(usr, SPAN_WARNING("A screen with graphics and walls of physics and engineering values open, you immediately force it closed."))
- return
- if(firemission_envelope.stat != FIRE_MISSION_STATE_IDLE)
- to_chat(usr, SPAN_WARNING("Fire Mission already underway."))
- return
- if(dropship.mode != SHUTTLE_CALL)
- to_chat(usr, SPAN_WARNING("Shuttle has to be in orbit."))
- return
- if(!firemission_envelope.recorded_loc)
- to_chat(usr, SPAN_WARNING("Target is not selected or lost."))
- return
-
- initiate_firemission()
-
- if(href_list["fm_weapon_id"])
- var/weap_ref = text2num(href_list["fm_weapon_id"])+1
- var/offset_ref = text2num(href_list["fm_offset_id"])+1
- var/mob/M = usr
- if(!skillcheck(M, SKILL_PILOT, SKILL_PILOT_TRAINED)) //only pilots can fire dropship weapons.
- to_chat(usr, SPAN_WARNING("A screen with graphics and walls of physics and engineering values open, you immediately force it closed."))
- return
- if(!editing_firemission)
- to_chat(usr, SPAN_WARNING("You are no longer editing Fire Mission."))
- return
- if(!editing_firemission.records || editing_firemission.records.len FIRE_MISSION_STATE_IN_TRANSIT && firemission_envelope.stat < FIRE_MISSION_STATE_COOLDOWN)
- to_chat(usr, SPAN_WARNING("Fire Mission already underway."))
- return
- var/list/gimb = record.get_offsets()
- var/min = gimb["min"]
- var/max = gimb["max"]
- var/offset_value = stripped_input(usr, "Enter offset for the [record.weapon.name]. It has to be between [min] and [max]. Enter '-' to remove fire order on this time stamp.", "Firing offset", "[record.offsets[offset_ref]]", 2)
- if(offset_value == null)
- return
- if(offset_value == "-")
- offset_value = "-"
- else
- offset_value = text2num(offset_value)
- if(offset_value == null)
- to_chat(usr, SPAN_WARNING("Incorrect offset value."))
+ simulate_firemission(user)
+ . = TRUE
+
+ if("switch_firemission")
+ configuration = tgui_input_list(user, "Select firemission to simulate", "Select firemission", firemission_envelope.missions, 30 SECONDS)
+ if(!selected_firemission)
+ to_chat(user, SPAN_WARNING("No configured firemission"))
return
- var/result = firemission_envelope.update_mission(firemission_envelope.missions.Find(editing_firemission), weap_ref, offset_ref, offset_value, TRUE)
- if(result == 0)
- to_chat(usr, SPAN_WARNING("Update caused an error: [firemission_envelope.mission_error]"))
- if(result == -1)
- to_chat(usr, SPAN_WARNING("System Error. Delete this Fire Mission."))
-
- if(href_list["firemission_camera"])
- if(dropship.mode != SHUTTLE_CALL)
- to_chat(usr, SPAN_WARNING("Shuttle has to be in orbit."))
- return
-
- if(!firemission_envelope.guidance)
- to_chat(usr, SPAN_WARNING("Guidance is not selected or lost."))
- return
-
- firemission_envelope.add_user_to_tracking(usr)
-
- to_chat(usr, "You peek through the guidance camera.")
-
- if(href_list["cas_camera"])
- if(!ishuman(usr))
- to_chat(usr, SPAN_WARNING("You have no idea how to do that!"))
- return
- if(dropship.mode != SHUTTLE_CALL)
- to_chat(usr, SPAN_WARNING("Shuttle has to be in orbit."))
- return
-
- if(!faction)
- to_chat(usr, SPAN_DANGER("Bug encountered, this console doesn't have a faction set, report this to a coder!"))
- return
-
- var/datum/cas_iff_group/cas_group = cas_groups[faction]
- if(!cas_group)
- to_chat(usr, SPAN_DANGER("Bug encountered, no CAS group exists for this console, report this to a coder!"))
- return
-
- var/targ_id = text2num(href_list["cas_camera"])
-
- var/datum/cas_signal/new_signal
- for(var/datum/cas_signal/LT as anything in cas_group.cas_signals)
- if(LT.target_id == targ_id && LT.valid_signal())
- new_signal = LT
- break
-
- if(!new_signal)
- to_chat(usr, SPAN_WARNING("Target lost or obstructed."))
- return
-
- if(usr in selected_cas_signal?.linked_cam?.viewing_users) // Reset previous cam
- remove_from_view(usr)
-
- selected_cas_signal = new_signal
- if(selected_cas_signal && selected_cas_signal.linked_cam)
- selected_cas_signal.linked_cam.view_directly(usr)
- else
- to_chat(usr, SPAN_WARNING("Error!"))
- return
- give_action(usr, /datum/action/human_action/cancel_view)
- RegisterSignal(usr, COMSIG_MOB_RESET_VIEW, PROC_REF(remove_from_view))
- RegisterSignal(usr, COMSIG_MOB_RESISTED, PROC_REF(remove_from_view))
- firemission_envelope.apply_upgrade(usr)
- to_chat(usr, SPAN_NOTICE("You peek through the guidance camera."))
-
- ui_interact(usr)
-
-/obj/structure/machinery/computer/dropship_weapons/proc/remove_from_view(mob/living/carbon/human/user)
- UnregisterSignal(user, COMSIG_MOB_RESET_VIEW)
- UnregisterSignal(user, COMSIG_MOB_RESISTED)
- if(selected_cas_signal && selected_cas_signal.linked_cam)
- selected_cas_signal.linked_cam.remove_from_view(user)
- firemission_envelope.remove_upgrades(user)
-
-/obj/structure/machinery/computer/dropship_weapons/proc/initiate_firemission()
- set waitfor = 0
- var/obj/docking_port/mobile/marine_dropship/dropship = SSshuttle.getShuttle(shuttle_tag)
- if (!istype(dropship))
+ if(!configuration)
+ configuration = selected_firemission
+ . = TRUE
+
+ if("switchmode")
+ simulation.dummy_mode = tgui_input_list(user, "Select target type to simulate", "Target type", simulation.target_types, 30 SECONDS)
+ if(!simulation.dummy_mode)
+ simulation.dummy_mode = CLF_MODE
+ . = TRUE
+
+ if("set-camera")
+ var/target_camera = params["equipment_id"]
+ set_camera_target(target_camera)
+ return TRUE
+
+ if("set-camera-sentry")
+ var/equipment_tag = params["equipment_id"]
+ for(var/obj/structure/dropship_equipment/equipment as anything in shuttle.equipments)
+ var/mount_point = equipment.ship_base.attach_id
+ if(mount_point != equipment_tag)
+ continue
+ if(istype(equipment, /obj/structure/dropship_equipment/sentry_holder))
+ var/obj/structure/dropship_equipment/sentry_holder/sentry = equipment
+ var/obj/structure/machinery/defenses/sentry/defense = sentry.deployed_turret
+ if(defense.has_camera)
+ defense.setup_target_acquisition()
+ camera_area_equipment = sentry
+ SEND_SIGNAL(src, COMSIG_CAMERA_SET_AREA, defense.loc.x, defense.loc.y, defense.loc.z, 11, 11)
+ return TRUE
+
+ if("clear-camera")
+ set_camera_target(null)
+ return TRUE
+
+ if("medevac-target")
+ var/equipment_tag = params["equipment_id"]
+ for(var/obj/structure/dropship_equipment/equipment as anything in shuttle.equipments)
+ var/mount_point = equipment.ship_base.attach_id
+ if(mount_point != equipment_tag)
+ continue
+ if (istype(equipment, /obj/structure/dropship_equipment/medevac_system))
+ var/obj/structure/dropship_equipment/medevac_system/medevac = equipment
+ var/target_ref = params["ref"]
+ medevac.automate_interact(user, target_ref)
+ if(medevac.linked_stretcher)
+ SEND_SIGNAL(src, COMSIG_CAMERA_SET_TARGET, medevac.linked_stretcher, 5, 5)
+ return TRUE
+
+ if("fulton-target")
+ var/equipment_tag = params["equipment_id"]
+ for(var/obj/structure/dropship_equipment/equipment as anything in shuttle.equipments)
+ var/mount_point = equipment.ship_base.attach_id
+ if(mount_point != equipment_tag)
+ continue
+ if (istype(equipment, /obj/structure/dropship_equipment/fulton_system))
+ var/obj/structure/dropship_equipment/fulton_system/fulton = equipment
+ var/target_ref = params["ref"]
+ fulton.automate_interact(user, target_ref)
+ return TRUE
+
+ if("fire-weapon")
+ var/weapon_tag = params["eqp_tag"]
+ var/obj/structure/dropship_equipment/weapon/DEW = get_weapon(weapon_tag)
+ if(!DEW)
+ return FALSE
+
+ var/datum/cas_signal/sig = get_cas_signal(camera_target_id)
+ if(!sig)
+ return FALSE
+
+ selected_equipment = DEW
+ if(ui_open_fire(user, shuttle, camera_target_id))
+ if(firemission_envelope)
+ firemission_envelope.untrack_object()
+ return TRUE
+
+ if("deploy-equipment")
+ var/equipment_tag = params["equipment_id"]
+ for(var/obj/structure/dropship_equipment/equipment as anything in shuttle.equipments)
+ var/mount_point = equipment.ship_base.attach_id
+ if(mount_point != equipment_tag)
+ continue
+ if(camera_area_equipment == equipment)
+ set_camera_target(null)
+ equipment.equipment_interact(user)
+ return TRUE
+
+ if("firemission-create")
+ var/name = params["firemission_name"]
+ var/length = params["firemission_length"]
+ var/length_n = text2num(length)
+ if(!length_n)
+ to_chat(user, SPAN_WARNING("Incorrect input format."))
+ return FALSE
+ ui_create_firemission(user, name, length_n)
+ return TRUE
+
+ if("firemission-delete")
+ var/name = params["firemission_name"]
+ ui_delete_firemission(user, name)
+ return TRUE
+
+ if("firemission-dual-offset-camera")
+ var/target_id = params["target_id"]
+
+ var/x_offset_value = params["x_offset_value"]
+ var/y_offset_value = params["y_offset_value"]
+
+ camera_target_id = target_id
+ var/datum/cas_signal/cas_sig = get_cas_signal(camera_target_id, valid_only = TRUE)
+ // we don't want rapid offset changes to trigger admin warnings
+ // and block the user from accessing TGUI
+ // we change the minute_count
+ user.client.reduce_minute_count()
+ if(!cas_sig)
+ return TRUE
+
+ // find position of cas_sig with offset dir and value applied
+ var/dx = text2num(x_offset_value)
+ var/dy = text2num(y_offset_value)
+
+ var/obj/current = cas_sig.signal_loc
+ var/obj/new_target = locate(
+ current.x + dx,
+ current.y + dy,
+ current.z)
+
+ camera_area_equipment = null
+ firemission_envelope.change_current_loc(new_target, cas_sig)
+ return TRUE
+
+ if("nvg-enable")
+ SEND_SIGNAL(src, COMSIG_CAMERA_SET_NVG, 5, "#7aff7a")
+ return TRUE
+
+ if("nvg-disable")
+ SEND_SIGNAL(src, COMSIG_CAMERA_CLEAR_NVG)
+ return TRUE
+
+ if("firemission-edit")
+ var/fm_tag = text2num(params["tag"])
+ var/weapon_id = text2num(params["weapon_id"])
+ var/offset_id = text2num(params["offset_id"])
+ var/offset_value = text2num(params["offset_value"])
+ return ui_firemission_change_offset(user, fm_tag, weapon_id, offset_id + 1, offset_value)
+
+ if("firemission-execute")
+ var/fm_tag = text2num(params["tag"])
+ var/direction = params["direction"]
+ var/target_id = params["target_id"]
+ var/offset_x_value = params["offset_x_value"]
+ var/offset_y_value = params["offset_y_value"]
+
+ if(!ui_select_firemission(user, fm_tag))
+ playsound(src, 'sound/machines/terminal_error.ogg', 5, 1)
+ return FALSE
+ if(!update_direction(user, text2num(direction)))
+ playsound(src, 'sound/machines/terminal_error.ogg', 5, 1)
+ return FALSE
+ if(!ui_select_laser_firemission(user, shuttle, target_id))
+ playsound(src, 'sound/machines/terminal_error.ogg', 5, 1)
+ return FALSE
+
+ initiate_firemission(user, fm_tag, direction, text2num(offset_x_value), text2num(offset_y_value))
+ return TRUE
+ if("paradrop-lock")
+ var/obj/docking_port/mobile/marine_dropship/linked_shuttle = SSshuttle.getShuttle(shuttle_tag)
+ if(!linked_shuttle)
+ return FALSE
+ if(linked_shuttle.mode != SHUTTLE_CALL)
+ return FALSE
+ if(linked_shuttle.paradrop_signal)
+ clear_locked_turf_and_lock_aft()
+ return TRUE
+ var/datum/cas_signal/sig = get_cas_signal(camera_target_id)
+ if(!sig)
+ to_chat(user, SPAN_WARNING("No signal chosen."))
+ return FALSE
+ var/turf/location = get_turf(sig.signal_loc)
+ var/area/location_area = get_area(location)
+ if(CEILING_IS_PROTECTED(location_area.ceiling, CEILING_PROTECTION_TIER_1))
+ to_chat(user, SPAN_WARNING("Target is obscured."))
+ return FALSE
+ var/equipment_tag = params["equipment_id"]
+ for(var/obj/structure/dropship_equipment/equipment as anything in shuttle.equipments)
+ var/mount_point = equipment.ship_base.attach_id
+ if(mount_point != equipment_tag)
+ continue
+ if(istype(equipment, /obj/structure/dropship_equipment/paradrop_system))
+ var/obj/structure/dropship_equipment/paradrop_system/paradrop_system = equipment
+ if(paradrop_system.system_cooldown > world.time)
+ to_chat(user, SPAN_WARNING("You toggled the system too recently."))
+ return
+ paradrop_system.system_cooldown = world.time + 5 SECONDS
+ paradrop_system.visible_message(SPAN_NOTICE("[equipment] hums as it locks to a signal."))
+ break
+ linked_shuttle.paradrop_signal = sig
+ addtimer(CALLBACK(src, PROC_REF(open_aft_for_paradrop)), 2 SECONDS)
+ RegisterSignal(linked_shuttle.paradrop_signal, COMSIG_PARENT_QDELETING, PROC_REF(clear_locked_turf_and_lock_aft))
+ RegisterSignal(linked_shuttle, COMSIG_SHUTTLE_SETMODE, PROC_REF(clear_locked_turf_and_lock_aft))
+ return TRUE
+
+/obj/structure/machinery/computer/dropship_weapons/proc/open_aft_for_paradrop()
+ var/obj/docking_port/mobile/marine_dropship/shuttle = SSshuttle.getShuttle(shuttle_tag)
+ if(!shuttle || !shuttle.paradrop_signal || shuttle.mode != SHUTTLE_CALL)
return
- if (dropship.timer && dropship.timeLeft(1) < firemission_envelope.get_total_duration())
- to_chat(usr, "Not enough time to complete the Fire Mission")
+ shuttle.door_control.control_doors("force-unlock", "aft", TRUE)
+
+/obj/structure/machinery/computer/dropship_weapons/proc/clear_locked_turf_and_lock_aft()
+ SIGNAL_HANDLER
+ var/obj/docking_port/mobile/marine_dropship/shuttle = SSshuttle.getShuttle(shuttle_tag)
+ if(!shuttle)
return
- if (!dropship.in_flyby || dropship.mode != SHUTTLE_CALL)
- to_chat(usr, "Has to be in Fly By mode")
+ shuttle.door_control.control_doors("force-lock", "aft", TRUE)
+ visible_message(SPAN_WARNING("[src] displays an alert as it loses the paradrop target."))
+ for(var/obj/structure/dropship_equipment/paradrop_system/parad in shuttle.equipments)
+ parad.visible_message(SPAN_WARNING("[parad] displays an alert as it loses the paradrop target."))
+ UnregisterSignal(shuttle.paradrop_signal, COMSIG_PARENT_QDELETING)
+ UnregisterSignal(shuttle, COMSIG_SHUTTLE_SETMODE)
+ shuttle.paradrop_signal = null
+
+/obj/structure/machinery/computer/dropship_weapons/proc/get_weapon(eqp_tag)
+ var/obj/docking_port/mobile/marine_dropship/dropship = SSshuttle.getShuttle(shuttle_tag)
+ var/obj/structure/dropship_equipment/equipment = dropship.equipments[eqp_tag]
+ if(istype(equipment, /obj/structure/dropship_equipment/weapon))
+ //is weapon
+ return equipment
+ return
+
+/obj/structure/machinery/computer/dropship_weapons/proc/get_cas_signal(target_ref, valid_only = FALSE)
+ if(!target_ref)
return
- var/fmid = firemission_envelope.missions.Find(selected_firemission)
- if(!fmid)
- to_chat(usr, "No Firemission selected")
+ var/datum/cas_iff_group/cas_group = cas_groups[faction]
+ for(var/datum/cas_signal/sig in cas_group.cas_signals)
+ if(sig.target_id == target_ref)
+ if(valid_only && !sig.valid_signal())
+ continue
+ return sig
+
+/obj/structure/machinery/computer/dropship_weapons/proc/set_camera_target(target_ref)
+ camera_area_equipment = null
+ if(firemission_envelope)
+ firemission_envelope.untrack_object()
+
+ var/datum/cas_signal/target = get_cas_signal(target_ref)
+ camera_target_id = target_ref
+ if(!target)
+ SEND_SIGNAL(src, COMSIG_CAMERA_CLEAR)
return
- var/result = firemission_envelope.execute_firemission(firemission_envelope.recorded_loc, firemission_envelope.recorded_offset, firemission_envelope.recorded_dir, fmid)
- if(result<1)
- to_chat(usr, "Screen beeps with an error: "+ firemission_envelope.mission_error)
- else
- update_trace_loc()
+ var/cam_width = camera_width
+ var/cam_height = camera_height
+ if(upgraded == MATRIX_WIDE)
+ cam_width = cam_width * 1.5
+ cam_height = cam_height * 1.5
-/obj/structure/machinery/computer/dropship_weapons/proc/update_offset(new_offset)
- var/result = firemission_envelope.change_offset(new_offset)
- if(result<1)
- to_chat(usr, "Screen beeps with an error: "+ firemission_envelope.mission_error)
+ SEND_SIGNAL(src, COMSIG_CAMERA_SET_TARGET, target.linked_cam, cam_width, cam_height)
+
+/obj/structure/machinery/computer/dropship_weapons/proc/get_screen_mode()
+ . = 0
+ if(selected_equipment)
+ . = selected_equipment.screen_mode
+ if(editing_firemission && editing_firemission.check(src) != FIRE_MISSION_CODE_ERROR)
+ . = 2
+ if(selected_firemission && in_firemission_mode)
+ . = 3
+/obj/structure/machinery/computer/dropship_weapons/proc/get_firemission_data(mob/user)
+ . = list()
+ var/firemission_id = 1
+ for(var/datum/cas_fire_mission/firemission in firemission_envelope.missions)
+ var/error_code = firemission.check(src)
+
+ var/selected = firemission == selected_firemission
+ var/can_edit = error_code != FIRE_MISSION_CODE_ERROR && !selected
+
+ var/can_interact = firemission_envelope.stat == FIRE_MISSION_STATE_IDLE && error_code == FIRE_MISSION_ALL_GOOD
+ var/list/fm_data = firemission.ui_data(user)
+ fm_data["mission_tag"] = firemission_id
+ fm_data["can_edit"] = can_edit
+ fm_data["can_interact"] = can_interact
+ fm_data["selected"] = selected
+ . += list(fm_data)
+
+ firemission_id++
+
+/obj/structure/machinery/computer/dropship_weapons/proc/get_edit_firemission_data()
+ . = list()
+ if(!editing_firemission)
+ return
+ for(var/datum/cas_fire_mission_record/firerec as anything in editing_firemission.records)
+ var/gimbal = firerec.get_offsets()
+ var/ammo = firerec.get_ammo()
+ var/offsets = new /list(firerec.offsets.len)
+ for(var/idx = 1; idx < firerec.offsets.len; idx++)
+ offsets[idx] = firerec.offsets[idx] == null ? "-" : firerec.offsets[idx]
+ . += list(
+ "name" = sanitize(copytext(firerec.weapon.name, 1, 50)),
+ "ammo" = ammo,
+ "gimbal" = gimbal,
+ "offsets" = firerec.offsets
+ )
+
+/obj/structure/machinery/computer/dropship_weapons/proc/get_sanitised_equipment(mob/user, obj/docking_port/mobile/marine_dropship/dropship)
+ . = list()
+ var/element_nbr = 1
+ for(var/obj/structure/dropship_equipment/equipment in dropship.equipments)
+ var/list/data = list(
+ "name"= equipment.name,
+ "shorthand" = equipment.shorthand,
+ "eqp_tag" = element_nbr,
+ "is_weapon" = equipment.is_weapon,
+ "is_interactable" = equipment.is_interactable,
+ "mount_point" = equipment.ship_base.attach_id,
+ "is_missile" = istype(equipment, /obj/structure/dropship_equipment/weapon/rocket_pod),
+ "ammo_name" = equipment.ammo_equipped?.name,
+ "ammo" = equipment.ammo_equipped?.ammo_count,
+ "max_ammo" = equipment.ammo_equipped?.max_ammo_count,
+ "firemission_delay" = equipment.ammo_equipped?.fire_mission_delay,
+ "burst" = equipment.ammo_equipped?.ammo_used_per_firing,
+ "data" = equipment.ui_data(user)
+ )
+
+ . += list(data)
+
+ element_nbr++
+ equipment.linked_console = src
+
+
+/obj/structure/machinery/computer/dropship_weapons/proc/get_targets()
+ . = list()
+ var/datum/cas_iff_group/cas_group = cas_groups[faction]
+ for(var/datum/cas_signal/LT as anything in cas_group.cas_signals)
+ if(!istype(LT) || !LT.valid_signal())
+ continue
+ var/area/laser_area = get_area(LT.signal_loc)
+ . += list(
+ list(
+ "target_name" = "[LT.name] ([laser_area.name])",
+ "target_tag" = LT.target_id
+ )
+ )
+
+/obj/structure/machinery/computer/dropship_weapons/proc/ui_equip_interact(mob/user, base_tag)
+ var/obj/docking_port/mobile/marine_dropship/shuttle = SSshuttle.getShuttle(shuttle_tag)
+ var/obj/structure/dropship_equipment/E = shuttle.equipments[base_tag]
+ E.linked_console = src
+ E.equipment_interact(user)
+
+/obj/structure/machinery/computer/dropship_weapons/proc/ui_open_fire(mob/weapon_operator, obj/docking_port/mobile/marine_dropship/dropship, targ_id)
+ if(ishuman(weapon_operator))
+ var/mob/living/carbon/human/human_operator = weapon_operator
+ if(!human_operator.allow_gun_usage)
+ to_chat(human_operator, SPAN_WARNING("Your programming prevents you from operating dropship weaponry!"))
+ return FALSE
+ var/obj/structure/dropship_equipment/weapon/DEW = selected_equipment
+ if(!selected_equipment || !selected_equipment.is_weapon)
+ to_chat(weapon_operator, SPAN_WARNING("No weapon selected."))
+ return FALSE
+ if(!skillcheck(weapon_operator, SKILL_PILOT, DEW.skill_required)) //only pilots can fire dropship weapons.
+ to_chat(weapon_operator, SPAN_WARNING("You don't have the training to fire this weapon!"))
+ return FALSE
+ if(dropship.mode != SHUTTLE_CALL)
+ to_chat(weapon_operator, SPAN_WARNING("Dropship can only fire while in flight."))
+ return FALSE
+ if(!faction)
+ return FALSE//no faction, no weapons
+ if(!selected_equipment || !selected_equipment.is_weapon)
+ to_chat(weapon_operator, SPAN_WARNING("No weapon selected."))
+ return FALSE
+ if(dropship.door_override)
+ return FALSE
+ if(!skillcheck(weapon_operator, SKILL_PILOT, DEW.skill_required)) //only pilots can fire dropship weapons.
+ to_chat(weapon_operator, SPAN_WARNING("You don't have the training to fire this weapon!"))
+ return FALSE
+ if(!dropship.in_flyby && DEW.fire_mission_only)
+ to_chat(weapon_operator, SPAN_WARNING("[DEW] requires a fire mission flight type to be fired."))
+ return FALSE
+
+ if(!DEW.ammo_equipped || DEW.ammo_equipped.ammo_count <= 0)
+ to_chat(weapon_operator, SPAN_WARNING("[DEW] has no ammo."))
+ return FALSE
+ if(DEW.last_fired > world.time - DEW.firing_delay)
+ to_chat(weapon_operator, SPAN_WARNING("[DEW] just fired, wait for it to cool down."))
+ return FALSE
+
+ var/datum/cas_iff_group/cas_group = cas_groups[faction]
+
+ if(!cas_group)
+ return FALSE//broken group. No fighting
+
+ for(var/datum/cas_signal/LT in cas_group.cas_signals)
+ if(LT.target_id != targ_id || !LT.valid_signal())
+ continue
+ if(!LT.signal_loc)
+ return FALSE
+ var/turf/TU = get_turf(LT.signal_loc)
+ var/area/targ_area = get_area(LT.signal_loc)
+ var/is_outside = FALSE
+ if(is_ground_level(TU.z))
+ switch(targ_area.ceiling)
+ if(CEILING_NONE)
+ is_outside = TRUE
+ if(CEILING_GLASS)
+ is_outside = TRUE
+ if(!is_outside && !cavebreaker) //cavebreaker doesn't care
+ to_chat(weapon_operator, SPAN_WARNING("INVALID TARGET: target must be visible from high altitude."))
+ return FALSE
+ if (protected_by_pylon(TURF_PROTECTION_CAS, TU))
+ to_chat(weapon_operator, SPAN_WARNING("INVALID TARGET: biological-pattern interference with signal."))
+ return FALSE
+ if(!DEW.ammo_equipped.can_fire_at(TU, weapon_operator))
+ return FALSE
+
+ DEW.open_fire(LT.signal_loc)
+ return TRUE
+ return FALSE
+
+/obj/structure/machinery/computer/dropship_weapons/proc/ui_create_firemission(mob/weapon_operator, firemission_name, firemission_length)
+ if(!skillcheck(weapon_operator, SKILL_PILOT, SKILL_PILOT_TRAINED)) //only pilots can fire dropship weapons.
+ to_chat(weapon_operator, SPAN_WARNING("A screen with graphics and walls of physics and engineering values open, you immediately force it closed."))
+ return FALSE
+ // Check name
+ if(!firemission_name || length(firemission_name) < 1)
+ to_chat(weapon_operator, SPAN_WARNING("Name too short (at least 1 symbols)."))
+ return FALSE
+ // Check length
+ if(!firemission_length)
+ to_chat(weapon_operator, SPAN_WARNING("Incorrect input format."))
+ return FALSE
+ if(firemission_length > firemission_envelope.fire_length)
+ to_chat(weapon_operator, SPAN_WARNING("Fire Mission is longer than allowed by this vehicle."))
+ return FALSE
+ if(firemission_envelope.stat != FIRE_MISSION_STATE_IDLE)
+ to_chat(weapon_operator, SPAN_WARNING("Vehicle has to be idle to allow Fire Mission editing and creation."))
+ return FALSE
+
+ for(var/datum/cas_fire_mission/mission in firemission_envelope.missions)
+ if(firemission_name == mission.name)
+ to_chat(weapon_operator, SPAN_WARNING("Fire Mission name must be unique."))
+ return FALSE
+ //everything seems to be fine now
+ firemission_envelope.generate_mission(firemission_name, firemission_length)
+ return TRUE
+
+/obj/structure/machinery/computer/dropship_weapons/proc/ui_delete_firemission(mob/weapon_operator, firemission_tag)
+ if(!skillcheck(weapon_operator, SKILL_PILOT, SKILL_PILOT_TRAINED)) //only pilots can fire dropship weapons.
+ to_chat(weapon_operator, SPAN_WARNING("A screen with graphics and walls of physics and engineering values open, you immediately force it closed."))
+ return FALSE
+ if(firemission_tag > firemission_envelope.missions.len)
+ to_chat(weapon_operator, SPAN_WARNING("Fire Mission ID corrupted or already deleted."))
+ return FALSE
+ if(selected_firemission == firemission_envelope.missions[firemission_tag])
+ to_chat(weapon_operator, SPAN_WARNING("Can't delete selected Fire Mission."))
+ return FALSE
+ var/result = firemission_envelope.delete_firemission(firemission_tag)
+ if(result != 1)
+ to_chat(weapon_operator, SPAN_WARNING("Unable to delete Fire Mission while in combat."))
+ return FALSE
+ return TRUE
+
+/obj/structure/machinery/computer/dropship_weapons/proc/ui_select_firemission(mob/weapon_operator, firemission_tag)
+ if(!skillcheck(weapon_operator, SKILL_PILOT, SKILL_PILOT_TRAINED)) //only pilots can fire dropship weapons.
+ to_chat(weapon_operator, SPAN_WARNING("A screen with graphics and walls of physics and engineering values open, you immediately force it closed."))
+ return FALSE
+ if(firemission_envelope.stat > FIRE_MISSION_STATE_IN_TRANSIT && firemission_envelope.stat < FIRE_MISSION_STATE_COOLDOWN)
+ to_chat(weapon_operator, SPAN_WARNING("Fire Mission already underway."))
+ return FALSE
+ if(firemission_tag > firemission_envelope.missions.len)
+ to_chat(weapon_operator, SPAN_WARNING("Fire Mission ID corrupted or deleted."))
+ return FALSE
+ if(selected_firemission == firemission_envelope.missions[firemission_tag])
+ selected_firemission = null
else
- update_trace_loc()
+ selected_firemission = firemission_envelope.missions[firemission_tag]
+ return TRUE
+
+/obj/structure/machinery/computer/dropship_weapons/proc/ui_firemission_change_offset(mob/weapons_operator, fm_tag, weapon_id, offset_id, offset_value)
+ if(!skillcheck(weapons_operator, SKILL_PILOT, SKILL_PILOT_TRAINED)) //only pilots can fire dropship weapons.
+ to_chat(weapons_operator, SPAN_WARNING("A screen with graphics and walls of physics and engineering values open, you immediately force it closed."))
+ return FALSE
+
+ var/result = firemission_envelope.update_mission(fm_tag, weapon_id, offset_id, offset_value)
+ if(result != FIRE_MISSION_ALL_GOOD)
+ playsound(src, 'sound/machines/terminal_error.ogg', 5, 1)
+ return TRUE
+
+/obj/structure/machinery/computer/dropship_weapons/proc/ui_select_laser_firemission(mob/weapons_operator, obj/docking_port/mobile/marine_dropship/dropship, laser)
+ if(!laser)
+ to_chat(weapons_operator, SPAN_WARNING("Bad Target."))
+ return FALSE
+ if(!skillcheck(weapons_operator, SKILL_PILOT, SKILL_PILOT_TRAINED)) //only pilots can fire dropship weapons.
+ to_chat(weapons_operator, SPAN_WARNING("A screen with graphics and walls of physics and engineering values open, you immediately force it closed."))
+ return FALSE
+ if(firemission_envelope.stat > FIRE_MISSION_STATE_IN_TRANSIT && firemission_envelope.stat < FIRE_MISSION_STATE_COOLDOWN)
+ to_chat(weapons_operator, SPAN_WARNING("Fire Mission already underway."))
+ return FALSE
+ if(dropship.mode != SHUTTLE_CALL)
+ to_chat(weapons_operator, SPAN_WARNING("Shuttle has to be in orbit."))
+ return FALSE
+ var/datum/cas_iff_group/cas_group = cas_groups[faction]
+ var/datum/cas_signal/cas_sig
+ for(var/X in cas_group.cas_signals)
+ var/datum/cas_signal/LT = X
+ if(LT.target_id == laser && LT.valid_signal())
+ cas_sig = LT
+ if(!cas_sig)
+ to_chat(weapons_operator, SPAN_WARNING("Target lost or obstructed."))
+ return FALSE
+
+ update_location(weapons_operator, cas_sig)
+ return TRUE
+
+/obj/structure/machinery/computer/dropship_weapons/proc/initiate_firemission(mob/user, fmId, dir, offset_x, offset_y)
+ set waitfor = 0
+ var/obj/docking_port/mobile/marine_dropship/dropship = SSshuttle.getShuttle(shuttle_tag)
+ if (!istype(dropship))
+ return FALSE
+ if (!dropship.in_flyby || dropship.mode != SHUTTLE_CALL)
+ to_chat(user, SPAN_WARNING("Has to be in Fly By mode"))
+ return FALSE
+ if (dropship.timer && dropship.timeLeft(1) < firemission_envelope.flyoff_period)
+ to_chat(user, SPAN_WARNING("Not enough time to complete the Fire Mission"))
+ return FALSE
+ var/datum/cas_signal/recorded_loc = firemission_envelope.recorded_loc
+ var/obj/source = recorded_loc.signal_loc
+ var/turf/target = locate(
+ source.x + offset_x,
+ source.y + offset_y,
+ source.z
+ )
+ var/result = firemission_envelope.execute_firemission(recorded_loc, target, dir, fmId)
+ if(result != FIRE_MISSION_ALL_GOOD)
+ to_chat(user, SPAN_WARNING("Screen beeps with an error: [firemission_envelope.mission_error]"))
+ return TRUE
-/obj/structure/machinery/computer/dropship_weapons/proc/update_location(new_location)
+/obj/structure/machinery/computer/dropship_weapons/proc/update_location(mob/user, new_location)
var/result = firemission_envelope.change_target_loc(new_location)
if(result<1)
- to_chat(usr, "Screen beeps with an error: "+ firemission_envelope.mission_error)
- else
- update_trace_loc()
+ to_chat(user, SPAN_WARNING("Screen beeps with an error: [firemission_envelope.mission_error]"))
+ return FALSE
+ return TRUE
-/obj/structure/machinery/computer/dropship_weapons/proc/update_direction(new_direction)
+/obj/structure/machinery/computer/dropship_weapons/proc/update_direction(mob/user, new_direction)
var/result = firemission_envelope.change_direction(new_direction)
if(result<1)
- to_chat(usr, "Screen beeps with an error: " + firemission_envelope.mission_error)
- else
- update_trace_loc()
-
-/obj/structure/machinery/computer/dropship_weapons/on_unset_interaction(mob/user)
- ..()
- if(firemission_envelope && firemission_envelope.guidance)
- firemission_envelope.remove_user_from_tracking(user)
+ to_chat(user, SPAN_WARNING("Screen beeps with an error: [firemission_envelope.mission_error]"))
+ return FALSE
+ return TRUE
-/obj/structure/machinery/computer/dropship_weapons/proc/update_trace_loc()
+/obj/structure/machinery/computer/dropship_weapons/proc/update_trace_loc(mob/user)
if(!firemission_envelope)
return
if(firemission_envelope.recorded_loc == null || firemission_envelope.recorded_dir == null || firemission_envelope.recorded_offset == null)
return
if(firemission_envelope.recorded_loc.obstructed_signal())
- if(firemission_envelope.user_is_guided(usr))
- to_chat(usr, SPAN_WARNING("Signal Obstructed. You have to go in blind."))
+ if(firemission_envelope.user_is_guided(user))
+ to_chat(user, SPAN_WARNING("Signal Obstructed. You have to go in blind."))
return
var/sx = 0
var/sy = 0
@@ -714,102 +871,35 @@
return
var/area/laser_area = get_area(shootloc)
if(!istype(laser_area) || CEILING_IS_PROTECTED(laser_area.ceiling, CEILING_PROTECTION_TIER_1))
- if(firemission_envelope.user_is_guided(usr))
- to_chat(usr, SPAN_WARNING("Vision Obstructed. You have to go in blind."))
+ if(firemission_envelope.user_is_guided(user))
+ to_chat(user, SPAN_WARNING("Vision Obstructed. You have to go in blind."))
firemission_envelope.change_current_loc()
else
firemission_envelope.change_current_loc(shootloc)
-
-/obj/structure/machinery/computer/dropship_weapons/Destroy()
- . = ..()
-
- QDEL_NULL(firemission_envelope)
-
-/obj/structure/machinery/computer/dropship_weapons/midway
- name = "\improper 'Midway' weapons controls"
- shuttle_tag = DROPSHIP_MIDWAY
+ return TRUE
/obj/structure/machinery/computer/dropship_weapons/dropship1
name = "\improper 'Alamo' weapons controls"
+ req_one_access = list(ACCESS_MARINE_LEADER, ACCESS_MARINE_DROPSHIP, ACCESS_WY_FLIGHT)
+ firemission_envelope = new /datum/cas_fire_envelope/uscm_dropship()
shuttle_tag = DROPSHIP_ALAMO
/obj/structure/machinery/computer/dropship_weapons/dropship2
name = "\improper 'Normandy' weapons controls"
+ req_one_access = list(ACCESS_MARINE_LEADER, ACCESS_MARINE_DROPSHIP, ACCESS_WY_FLIGHT)
+ firemission_envelope = new /datum/cas_fire_envelope/uscm_dropship()
shuttle_tag = DROPSHIP_NORMANDY
-// CAS TGUI SHIT \\
-
-/obj/structure/machinery/computer/dropship_weapons/tgui_interact(mob/user, datum/tgui/ui)
- ui = SStgui.try_update_ui(user, src, ui)
- if(!ui)
- ui = new(user, src, "CasSim", "[src.name]")
- ui.open()
-
-/obj/structure/machinery/computer/dropship_weapons/ui_state(mob/user) // we gotta do custom shit here so that it always closes instead of suspending
- return GLOB.not_incapacitated_and_adjacent_strict_state
-
-/obj/structure/machinery/computer/dropship_weapons/ui_status(mob/user, datum/ui_state/state)
- . = ..()
- if(inoperable())
- return UI_CLOSE
-
-/obj/structure/machinery/computer/dropship_weapons/ui_data(mob/user)
- var/list/data = list()
-
- data["configuration"] = configuration
- data["looking"] = simulation.looking_at_simulation
- data["dummy_mode"] = simulation.dummy_mode
-
- data["worldtime"] = world.time
- data["nextdetonationtime"] = simulation.detonation_cooldown
- data["detonation_cooldown"] = simulation.detonation_cooldown_time
-
- return data
-
-/obj/structure/machinery/computer/dropship_weapons/ui_act(action, list/params, datum/tgui/ui, datum/ui_state/state)
- . = ..()
- if(.)
- return
-
- var/user = ui.user
-
- switch(action)
- if("start_watching")
- simulation.start_watching(user)
- . = TRUE
-
- if("stop_watching")
- simulation.stop_watching(user)
- . = TRUE
-
- if("execute_simulated_firemission")
- if(!configuration)
- to_chat(user, SPAN_WARNING("No configured firemission"))
- return
- simulate_firemission(user)
- . = TRUE
-
- if("switch_firemission")
- configuration = tgui_input_list(user, "Select firemission to simulate", "Select firemission", firemission_envelope.missions, 30 SECONDS)
- if(!selected_firemission)
- to_chat(user, SPAN_WARNING("No configured firemission"))
- return
- if(!configuration)
- configuration = selected_firemission
- . = TRUE
-
- if("switchmode")
- simulation.dummy_mode = tgui_input_list(user, "Select target type to simulate", "Target type", simulation.target_types, 30 SECONDS)
- if(!simulation.dummy_mode)
- simulation.dummy_mode = CLF_MODE
- . = TRUE
+/obj/structure/machinery/computer/dropship_weapons/midway
+ name = "\improper 'Midway' weapons controls"
+ req_one_access = list(ACCESS_MARINE_LEADER, ACCESS_MARINE_DROPSHIP, ACCESS_WY_FLIGHT)
+ firemission_envelope = new /datum/cas_fire_envelope/uscm_dropship()
+ shuttle_tag = DROPSHIP_MIDWAY
-/obj/structure/machinery/computer/dropship_weapons/ui_close(mob/user)
+/obj/structure/machinery/computer/dropship_weapons/Destroy()
. = ..()
- if(simulation.looking_at_simulation)
- simulation.stop_watching(user)
-
-// CAS TGUI SHIT END \\
+ QDEL_NULL(firemission_envelope)
+ QDEL_NULL(tacmap)
/obj/structure/machinery/computer/dropship_weapons/proc/simulate_firemission(mob/living/user)
diff --git a/code/game/machinery/doors/multi_tile.dm b/code/game/machinery/doors/multi_tile.dm
index c60c08dc03..0712d61f56 100644
--- a/code/game/machinery/doors/multi_tile.dm
+++ b/code/game/machinery/doors/multi_tile.dm
@@ -278,8 +278,8 @@
return
..()
-/obj/structure/machinery/door/airlock/multi_tile/almayer/dropshiprear/unlock()
- if(is_reserved_level(z))
+/obj/structure/machinery/door/airlock/multi_tile/almayer/dropshiprear/unlock(forced=FALSE)
+ if(is_reserved_level(z) && !forced)
return // in orbit
..()
@@ -293,6 +293,9 @@
if(xeno.action_busy)
return
+ if(is_reserved_level(z)) //no prying in space even though it's funny
+ return
+
var/direction
switch(id)
if("starboard_door")
diff --git a/code/game/machinery/vending/vendor_types/requisitions.dm b/code/game/machinery/vending/vendor_types/requisitions.dm
index 3591a7caec..aee17649e9 100644
--- a/code/game/machinery/vending/vendor_types/requisitions.dm
+++ b/code/game/machinery/vending/vendor_types/requisitions.dm
@@ -81,6 +81,7 @@
list("Technician Welder-Satchel", round(scale * 5), /obj/item/storage/backpack/marine/engineerpack/satchel, VENDOR_ITEM_REGULAR),
list("IMP Ammo Rack", round(scale * 2), /obj/item/storage/backpack/marine/ammo_rack, VENDOR_ITEM_REGULAR),
list("Radio Telephone Pack", round(scale * 2), /obj/item/storage/backpack/marine/satchel/rto, VENDOR_ITEM_REGULAR),
+ list("Parachute", round(scale * 20), /obj/item/parachute, VENDOR_ITEM_REGULAR),
list("BELTS", -1, null, null),
list("G8-A General Utility Pouch", round(scale * 2), /obj/item/storage/backpack/general_belt, VENDOR_ITEM_REGULAR),
@@ -93,7 +94,6 @@
list("M276 M82F Holster Rig", round(scale * 2), /obj/item/storage/belt/gun/flaregun, VENDOR_ITEM_REGULAR),
list("M276 Shotgun Shell Loading Rig", round(scale * 10), /obj/item/storage/belt/shotgun, VENDOR_ITEM_REGULAR),
list("M276 Mortar Operator Belt", round(scale * 2), /obj/item/storage/belt/gun/mortarbelt, VENDOR_ITEM_REGULAR),
- list("Rappel Harness", round(scale * 20), /obj/item/rappel_harness, VENDOR_ITEM_REGULAR),
list("POUCHES", -1, null, null),
list("Autoinjector Pouch", round(scale * 1), /obj/item/storage/pouch/autoinjector, VENDOR_ITEM_REGULAR),
diff --git a/code/game/objects/items/fulton.dm b/code/game/objects/items/fulton.dm
index 8b4b1f39da..c28d5e5d5a 100644
--- a/code/game/objects/items/fulton.dm
+++ b/code/game/objects/items/fulton.dm
@@ -140,10 +140,12 @@ var/global/list/deployed_fultons = list()
sleep(30)
original_location = get_turf(attached_atom)
playsound(loc, 'sound/items/fulton.ogg', 50, 1)
- reservation = SSmapping.RequestBlockReservation(3, 3, turf_type_override = /turf/open/space)
- var/middle_x = reservation.bottom_left_coords[1] + FLOOR((reservation.top_right_coords[1] - reservation.bottom_left_coords[1]) / 2, 1)
- var/middle_y = reservation.bottom_left_coords[2] + FLOOR((reservation.top_right_coords[2] - reservation.bottom_left_coords[2]) / 2, 1)
- var/turf/space_tile = locate(middle_x, middle_y, reservation.bottom_left_coords[3])
+ reservation = SSmapping.request_turf_block_reservation(3, 3, 1, turf_type_override = /turf/open/space)
+ var/turf/bottom_left_turf = reservation.bottom_left_turfs[1]
+ var/turf/top_right_turf = reservation.top_right_turfs[1]
+ var/middle_x = bottom_left_turf.x + floor((top_right_turf.x - bottom_left_turf.x) / 2)
+ var/middle_y = bottom_left_turf.y + floor((top_right_turf.y - bottom_left_turf.y) / 2)
+ var/turf/space_tile = locate(middle_x, middle_y, bottom_left_turf.z)
if(!space_tile)
visible_message(SPAN_WARNING("[src] begins beeping like crazy. Something is wrong!"))
return
diff --git a/code/game/objects/items/misc.dm b/code/game/objects/items/misc.dm
index dfbfbddff1..1bdcab2ac6 100644
--- a/code/game/objects/items/misc.dm
+++ b/code/game/objects/items/misc.dm
@@ -292,6 +292,16 @@
new /obj/item/evidencebag(src)
new /obj/item/evidencebag(src)
+/obj/item/parachute
+ name = "parachute"
+ desc = "A surprisingly small yet bulky pack with just enough safety straps to make RnD pass health and safety. The label says the pack comes with two parachutes - main and reserve, but you doubt the pack can fit even one."
+ icon = 'icons/obj/items/clothing/backpacks.dmi'
+ icon_state = "parachute_pack"
+ item_state = "parachute_pack"
+ w_class = SIZE_MASSIVE
+ flags_equip_slot = SLOT_BACK
+ flags_item = SMARTGUNNER_BACKPACK_OVERRIDE
+
/obj/item/rappel_harness
name = "rappel harness"
desc = "A simple, uncomfortable rappel harness with just enough safety straps to make RnD pass health and safety. It comes with an in-built descender, but has no pouches for ammunition."
@@ -318,7 +328,7 @@
to_chat(user, SPAN_WARNING("No shuttle detected in lower orbit, aborting extraction."))
return
- var/obj/structure/dropship_equipment/rappel_system/rapsys = locate() in shuttle.equipments
+ var/obj/structure/dropship_equipment/paradrop_system/rapsys = locate() in shuttle.equipments
if(!rapsys)
to_chat(user, SPAN_WARNING("No rappel system detected in shuttle, aborting extraction."))
return
@@ -356,7 +366,7 @@
H.equip_to_slot_if_possible(src, WEAR_WAIST)
try_extract(H)
-/obj/item/rappel_harness/extract/proc/on_extract(mob/living/carbon/human/user, obj/structure/dropship_equipment/rappel_system/system)
+/obj/item/rappel_harness/extract/proc/on_extract(mob/living/carbon/human/user, obj/structure/dropship_equipment/paradrop_system/system)
flick("rappel_hatch_opening", system)
user.pixel_x = 0
user.pixel_y = 0
diff --git a/code/game/objects/structures/stool_bed_chair_nest/bed.dm b/code/game/objects/structures/stool_bed_chair_nest/bed.dm
index a33d2889a4..95c5d8e45f 100644
--- a/code/game/objects/structures/stool_bed_chair_nest/bed.dm
+++ b/code/game/objects/structures/stool_bed_chair_nest/bed.dm
@@ -322,6 +322,7 @@ var/global/list/activated_medevac_stretchers = list()
base_bed_icon = "stretcher"
accepts_bodybag = TRUE
var/stretcher_activated
+ var/view_range = 5
var/obj/structure/dropship_equipment/medevac_system/linked_medevac
surgery_duration_multiplier = SURGERY_SURFACE_MULT_AWFUL //On the one hand, it's a big stretcher. On the other hand, you have a big sheet covering the patient and those damned Fulton hookups everywhere.
@@ -352,6 +353,14 @@ var/global/list/activated_medevac_stretchers = list()
toggle_medevac_beacon(usr)
+// Used to pretend to be a camera
+/obj/structure/bed/medevac_stretcher/proc/can_use()
+ return TRUE
+
+// Used to pretend to be a camera
+/obj/structure/bed/medevac_stretcher/proc/isXRay()
+ return FALSE
+
/obj/structure/bed/medevac_stretcher/proc/toggle_medevac_beacon(mob/user)
if(!ishuman(user))
return
diff --git a/code/game/sim_manager/datums/simulator.dm b/code/game/sim_manager/datums/simulator.dm
index 04ddb7faa0..5f3f48df27 100644
--- a/code/game/sim_manager/datums/simulator.dm
+++ b/code/game/sim_manager/datums/simulator.dm
@@ -1,18 +1,21 @@
+#define GRID_CLEARING_SIZE 16
+
/datum/simulator
// Necessary to prevent multiple users from simulating at the same time.
var/static/detonation_cooldown = 0
+ var/static/detonation_cooldown_time = 2 MINUTES
var/static/sim_reboot_state = TRUE
- var/looking_at_simulation = FALSE
- var/detonation_cooldown_time = 2 MINUTES
var/dummy_mode = CLF_MODE
var/obj/structure/machinery/camera/simulation/sim_camera
- var/grid_clearing_size = 16
// garbage collection,
var/static/list/delete_targets = list()
+ // list of users currently inside the simulator
+ var/static/list/users_in_sim = list()
+
/*
unarmoured humans are unnencessary clutter as they tend to explode easily
and litter the sim room with body parts, best left out.
@@ -29,7 +32,7 @@
/datum/simulator/proc/start_watching(mob/living/user)
- if(looking_at_simulation)
+ if(user in users_in_sim)
to_chat(user, SPAN_WARNING("You are already looking at the simulation."))
return
if(!sim_camera)
@@ -41,13 +44,15 @@
to_chat(user, SPAN_WARNING("You're too busy looking at something else."))
return
user.reset_view(sim_camera)
- looking_at_simulation = TRUE
+ users_in_sim += user
/datum/simulator/proc/stop_watching(mob/living/user)
+ if(!(user in users_in_sim))
+ return
user.unset_interaction()
user.reset_view(null)
user.cameraFollow = null
- looking_at_simulation = FALSE
+ users_in_sim -= user
/datum/simulator/proc/sim_turf_garbage_collection()
@@ -67,8 +72,8 @@
y:2 | x: 1 2 3 4 ... 16
y:1 | x: 1 2 3 4 ... 16
*/
- for (var/y_pos in 1 to grid_clearing_size)// outer y
- for (var/x_pos in 1 to grid_clearing_size) // inner x
+ for (var/y_pos in 1 to GRID_CLEARING_SIZE)// outer y
+ for (var/x_pos in 1 to GRID_CLEARING_SIZE) // inner x
var/turf/current_grid = locate(sim_grid_start_pos.x + x_pos,sim_grid_start_pos.y + y_pos,sim_grid_start_pos.z)
current_grid.empty(/turf/open/floor/engine)
@@ -101,3 +106,4 @@
addtimer(CALLBACK(src, PROC_REF(sim_turf_garbage_collection)), 30 SECONDS, TIMER_STOPPABLE)
+#undef GRID_CLEARING_SIZE
diff --git a/code/game/turfs/closed.dm b/code/game/turfs/closed.dm
index 41e055c5bf..7bbcc981de 100644
--- a/code/game/turfs/closed.dm
+++ b/code/game/turfs/closed.dm
@@ -15,6 +15,21 @@
icon_state = "black"
mouse_opacity = FALSE
+/// Cordon turf marking z-level boundaries and surrounding reservations
+/turf/closed/cordon
+ name = "world border"
+ icon = 'icons/turf/shuttle.dmi'
+ icon_state = "pclosed"
+ layer = ABOVE_TURF_LAYER
+ baseturfs = /turf/closed/cordon
+
+/// Used as placeholder turf when something went really wrong, as per /tg/ string lists handler
+/turf/closed/cordon/debug
+ name = "debug turf"
+ desc = "This turf shouldn't be here and probably result of incorrect turf replacement. Adminhelp about it or report it in an issue."
+ color = "#660088"
+ baseturfs = /turf/closed/cordon/debug
+
/turf/closed/mineral //mineral deposits
name = "Rock"
icon = 'icons/turf/walls/walls.dmi'
diff --git a/code/game/turfs/transit.dm b/code/game/turfs/transit.dm
index bae6718cfd..d3bad3bf95 100644
--- a/code/game/turfs/transit.dm
+++ b/code/game/turfs/transit.dm
@@ -11,19 +11,243 @@
if(isobserver(crosser) || crosser.anchored)
return
- if(!(isitem(crosser) || isliving(crosser)))
+ if(!isobj(crosser) && !isliving(crosser))
return
- var/turf/open/floor/floor = old_loc
- if(istype(floor))
- var/fling_dir = get_dir(floor, crosser.loc)
+ if(!istype(old_loc, /turf/open/space))
+ var/turf/projected = get_ranged_target_turf(crosser.loc, dir, 10)
- var/turf/near_turf = get_step(crosser.loc, get_step(crosser.loc, fling_dir))
- var/turf/projected = get_ranged_target_turf(near_turf, fling_dir, 50)
+ INVOKE_ASYNC(crosser, TYPE_PROC_REF(/atom/movable, throw_atom), projected, 50, SPEED_FAST, null, TRUE)
+
+ addtimer(CALLBACK(src, PROC_REF(handle_crosser), crosser), 0.5 SECONDS)
+
+/turf/open/space/transit/proc/handle_crosser(atom/movable/crosser)
+ if(QDELETED(crosser))
+ return
+ if(crosser.can_paradrop()) //let's not delete people who arent meant to be deleted... This shouldn't happen normally, but if it does, congratulations, you gamed the system
+ return
+ qdel(crosser)
+/turf/open/space/transit/dropship
+ var/shuttle_tag
+
+/turf/open/space/transit/dropship/handle_crosser(atom/movable/crosser)
+ if(QDELETED(crosser))
+ return
+ if(!shuttle_tag)
+ return ..()
+
+ var/obj/docking_port/mobile/marine_dropship/dropship = SSshuttle.getShuttle(shuttle_tag)
+ if(!istype(dropship) || dropship.mode != SHUTTLE_CALL)
+ return ..()
+
+ // you just jumped out of a dropship heading towards the LZ, have fun living on the way down!
+ var/list/ground_z_levels = SSmapping.levels_by_trait(ZTRAIT_GROUND)
+ if(!length(ground_z_levels))
+ return ..()
+
+ if(dropship.paradrop_signal) //if dropship in paradrop mode, drop them near the signal. Whether they have a parachute or not
+ var/list/valid_turfs = list()
+ var/turf/location = get_turf(dropship.paradrop_signal.signal_loc)
+ for(var/turf/turf as anything in RANGE_TURFS(crosser.get_paradrop_scatter(), location))
+ var/area/turf_area = get_area(turf)
+ if(!turf_area || CEILING_IS_PROTECTED(turf_area.ceiling, CEILING_PROTECTION_TIER_1))
+ continue
+ if(turf.density)
+ continue
+ var/found_dense = FALSE
+ for(var/atom/turf_atom in turf)
+ if(turf_atom.density && turf_atom.can_block_movement)
+ found_dense = TRUE
+ break
+ if(found_dense)
+ continue
+ if(protected_by_pylon(TURF_PROTECTION_MORTAR, turf))
+ continue
+ valid_turfs += turf
+ var/turf/deploy_turf
+ if(length(valid_turfs)) //if we found a fitting place near the landing zone...
+ deploy_turf = pick(valid_turfs)
+ else //if we somehow did not. Drop them right on the signal then, there is nothing we can do
+ deploy_turf = location
+ if(crosser.can_paradrop())
+ INVOKE_ASYNC(crosser, TYPE_PROC_REF(/atom/movable, handle_paradrop), deploy_turf, dropship.name)
+ return
+ INVOKE_ASYNC(crosser, TYPE_PROC_REF(/atom/movable, handle_airdrop), deploy_turf, dropship.name)
+ return
+
+ //find a random spot to drop them
+ var/list/area/potential_areas = shuffle(SSmapping.areas_in_z["[ground_z_levels[1]]"])
+
+ for(var/area/maybe_this_area in potential_areas)
+ if(CEILING_IS_PROTECTED(maybe_this_area.ceiling, CEILING_PROTECTION_TIER_1)) // prevents out of bounds too
+ continue
+ if(istype(maybe_this_area, /area/space)) // make sure its not space, just in case
+ continue
+
+ var/turf/open/possible_turf = null
+ var/list/area_turfs = get_area_turfs(maybe_this_area)
+ for(var/i in 1 to 10)
+ possible_turf = pick_n_take(area_turfs)
+ // we're looking for an open, non-dense, and non-space turf.
+ if(!istype(possible_turf) || is_blocked_turf(possible_turf) || istype(possible_turf, /turf/open/space))
+ continue
+
+ if(!istype(possible_turf) || is_blocked_turf(possible_turf) || istype(possible_turf, /turf/open/space))
+ continue // couldnt find one in 10 loops, check another area
+
+ // we found a good turf, lets drop em
+ if(crosser.can_paradrop())
+ INVOKE_ASYNC(crosser, TYPE_PROC_REF(/atom/movable, handle_paradrop), possible_turf, dropship.name)
+ return
+ INVOKE_ASYNC(crosser, TYPE_PROC_REF(/atom/movable, handle_airdrop), possible_turf, dropship.name)
+ return
+
+ //we didn't find a turf to drop them... This shouldn't happen usually
+ if(crosser.can_paradrop()) //don't delete them if they were supposed to paradrop
+ to_chat(crosser, SPAN_BOLDWARNING("Your harness got stuck and you got thrown back in the dropship."))
+ var/turf/projected = get_ranged_target_turf(crosser.loc, turn(dir, 180), 15)
INVOKE_ASYNC(crosser, TYPE_PROC_REF(/atom/movable, throw_atom), projected, 50, SPEED_FAST, null, TRUE)
+ return
+ return ..() // they couldn't be dropped, just delete them
+
+/atom/movable/proc/can_paradrop()
+ return FALSE
+
+/atom/movable/proc/get_paradrop_scatter()
+ return 7
+
+/mob/living/carbon/human/can_paradrop()
+ if(istype(back, /obj/item/parachute))
+ return TRUE
+ return ..()
+
+/obj/structure/closet/crate/can_paradrop() //for now all crates can be paradropped
+ return TRUE
+
+/obj/structure/closet/crate/get_paradrop_scatter() //crates land closer to the signal
+ return 4
+
+/obj/structure/largecrate/can_paradrop()
+ return TRUE
+
+/obj/structure/largecrate/get_paradrop_scatter()
+ return 4
- QDEL_IN(crosser, 0.5 SECONDS)
+/atom/movable/proc/handle_paradrop(turf/target, dropship_name)
+ clear_active_explosives()
+ ADD_TRAIT(src, TRAIT_IMMOBILIZED, TRAIT_SOURCE_DROPSHIP_INTERACTION)
+ ADD_TRAIT(src, TRAIT_UNDENSE, TRAIT_SOURCE_DROPSHIP_INTERACTION)
+ var/image/cables = image('icons/obj/structures/droppod_32x64.dmi', src, "chute_cables_static")
+ overlays += cables
+ var/image/chute = image('icons/obj/structures/droppod_64x64.dmi', src, "chute_static")
+
+ chute.pixel_x -= 16
+ chute.pixel_y += 16
+
+ overlays += chute
+ pixel_z = 360
+ forceMove(target)
+ playsound(src, 'sound/items/fulton.ogg', 30, 1)
+ animate(src, time = 3.5 SECONDS, pixel_z = 0, flags = ANIMATION_PARALLEL)
+ addtimer(CALLBACK(target, TYPE_PROC_REF(/turf, ceiling_debris)), 2 SECONDS)
+ addtimer(CALLBACK(src, PROC_REF(clear_parachute), cables, chute), 3.5 SECONDS)
+
+/mob/living/carbon/handle_paradrop(turf/target, dropship_name)
+ ..()
+ if(client)
+ playsound_client(client, 'sound/items/fulton.ogg', src, 50, 1) //for some reason you don't hear the sound while dropping, maybe because of force move?
+
+/atom/movable/proc/clear_parachute(image/cables, image/chute)
+ if(QDELETED(src))
+ return
+ REMOVE_TRAIT(src, TRAIT_IMMOBILIZED, TRAIT_SOURCE_DROPSHIP_INTERACTION)
+ REMOVE_TRAIT(src, TRAIT_UNDENSE, TRAIT_SOURCE_DROPSHIP_INTERACTION)
+ overlays -= cables
+ overlays -= chute
+
+/atom/movable/proc/clear_active_explosives()
+ for(var/obj/item/explosive/explosive in contents)
+ if(!explosive.active)
+ continue
+ explosive.deconstruct(FALSE)
+
+/atom/movable/proc/handle_airdrop(turf/target, dropship_name)
+ clear_active_explosives()
+ ADD_TRAIT(src, TRAIT_IMMOBILIZED, TRAIT_SOURCE_DROPSHIP_INTERACTION)
+ pixel_z = 360
+ forceMove(target)
+ animate(src, time = 6, pixel_z = 0, flags = ANIMATION_PARALLEL)
+ INVOKE_ASYNC(target, TYPE_PROC_REF(/turf, ceiling_debris))
+ REMOVE_TRAIT(src, TRAIT_IMMOBILIZED, TRAIT_SOURCE_DROPSHIP_INTERACTION)
+
+/obj/handle_airdrop(turf/target, dropship_name)
+ ..()
+ if(!indestructible && prob(30)) // throwing objects from the air is not always a good idea
+ deconstruct(FALSE)
+
+/obj/structure/closet/handle_airdrop(turf/target, dropship_name) // good idea but no
+ if(!opened)
+ for(var/atom/movable/content in src)
+ INVOKE_ASYNC(content, TYPE_PROC_REF(/atom/movable, handle_airdrop), target, dropship_name)
+ open()
+ . = ..()
+
+/obj/item/handle_airdrop(turf/target, dropship_name)
+ ..()
+ if(QDELETED(src))
+ return
+ if(!indestructible && w_class < SIZE_MEDIUM) //tiny and small items will be lost, good riddance
+ deconstruct(FALSE)
+ return
+ explosion_throw(200) // give it a bit of a kick
+
+/obj/item/explosive/handle_airdrop(turf/target, dropship_name)
+ if(active)
+ deconstruct(FALSE)
+ return
+ ..()
+
+/mob/living/handle_airdrop(turf/target, dropship_name)
+ ..()
+ playsound(target, "punch", rand(20, 70), TRUE)
+ playsound(target, "punch", rand(20, 70), TRUE)
+ playsound(target, "bone_break", rand(20, 70), TRUE)
+ playsound(target, "bone_break", rand(20, 70), TRUE)
+
+ KnockDown(10)
+ Stun(3)
+ // take a little bit more damage otherwise
+ take_overall_damage(400, used_weapon = "falling", limb_damage_chance = 100)
+ visible_message(SPAN_WARNING("[src] falls out of the sky."), SPAN_HIGHDANGER("As you fall out of the [dropship_name], you plummet towards the ground."))
+
+/mob/living/carbon/human/handle_airdrop(turf/target, dropship_name)
+ ..()
+ last_damage_data = create_cause_data("falling from [dropship_name]", src)
+ // I'd say falling from space is pretty much like getting hit by an explosion
+ take_overall_armored_damage(300, ARMOR_BOMB, limb_damage_chance = 100)
+ // but just in case, you will still take a ton of damage.
+ take_overall_damage(200, used_weapon = "falling", limb_damage_chance = 100)
+ if(stat < DEAD)
+ death(last_damage_data)
+ status_flags |= PERMANENTLY_DEAD
+
+
+/turf/open/space/transit/dropship/alamo
+ shuttle_tag = DROPSHIP_ALAMO
+ dir = SOUTH
+
+/turf/open/space/transit/dropship/normandy
+ shuttle_tag = DROPSHIP_NORMANDY
+ dir = SOUTH
+
+/turf/open/space/transit/dropship/midway
+ shuttle_tag = DROPSHIP_MIDWAY
+ dir = SOUTH
+
+/turf/open/space/transit/dropship/cyclone
+ shuttle_tag = DROPSHIP_CYCLONE
+ dir = SOUTH
/turf/open/space/transit/south
dir = SOUTH
diff --git a/code/game/turfs/turf.dm b/code/game/turfs/turf.dm
index 837610d5d7..16db20e298 100644
--- a/code/game/turfs/turf.dm
+++ b/code/game/turfs/turf.dm
@@ -24,7 +24,6 @@
*/
-
/turf
icon = 'icons/turf/floors/floors.dmi'
var/intact_tile = 1 //used by floors to distinguish floor with/without a floortile(e.g. plating).
@@ -47,7 +46,7 @@
var/changing_turf = FALSE
var/chemexploded = FALSE // Prevents explosion stacking
- var/flags_turf = NO_FLAGS
+ var/turf_flags = NO_FLAGS
/// Whether we've broken through the ceiling yet
var/ceiling_debrised = FALSE
@@ -57,6 +56,7 @@
///Lumcount added by sources other than lighting datum objects, such as the overlay lighting component.
var/dynamic_lumcount = 0
+
///List of light sources affecting this turf.
///Which directions does this turf block the vision of, taking into account both the turf's opacity and the movable opacity_sources.
var/directional_opacity = NONE
@@ -149,6 +149,22 @@
/turf/proc/update_icon() //Base parent. - Abby
return
+/// Call to move a turf from its current area to a new one
+/turf/proc/change_area(area/old_area, area/new_area)
+ //dont waste our time
+ if(old_area == new_area)
+ return
+
+ //move the turf
+ new_area.contents += src
+
+ //changes to make after turf has moved
+ on_change_area(old_area, new_area)
+
+/// Allows for reactions to an area change without inherently requiring change_area() be called (I hate maploading)
+/turf/proc/on_change_area(area/old_area, area/new_area)
+ transfer_area_lighting(old_area, new_area)
+
/turf/proc/add_cleanable_overlays()
for(var/cleanable_type in cleanables)
var/obj/effect/decal/cleanable/C = cleanables[cleanable_type]
@@ -439,6 +455,10 @@
W.levelupdate()
return W
+//If you modify this function, ensure it works correctly with lateloaded map templates.
+/turf/proc/AfterChange(flags, oldType) //called after a turf has been replaced in ChangeTurf()
+ return // Placeholder. This is mostly used by /tg/ code for atmos updates
+
// Take off the top layer turf and replace it with the next baseturf down
/turf/proc/ScrapeAway(amount=1, flags)
if(!amount)
@@ -771,6 +791,33 @@ GLOBAL_LIST_INIT(blacklisted_automated_baseturfs, typecacheof(list(
change_type = new_baseturfs
return ChangeTurf(change_type, null, flags)
+/// Places a turf on top - for map loading
+/turf/proc/load_on_top(turf/added_layer, flags)
+ var/area/our_area = get_area(src)
+ flags = our_area.PlaceOnTopReact(list(baseturfs), added_layer, flags)
+
+ if(flags & CHANGETURF_SKIP) // We haven't been initialized
+ if(flags_atom & INITIALIZED)
+ stack_trace("CHANGETURF_SKIP was used in a PlaceOnTop call for a turf that's initialized. This is a mistake. [src]([type])")
+ assemble_baseturfs()
+
+ var/turf/new_turf
+ if(!length(baseturfs))
+ baseturfs = list(baseturfs)
+
+ var/list/old_baseturfs = baseturfs.Copy()
+ if(!isclosedturf(src))
+ old_baseturfs += type
+
+ new_turf = ChangeTurf(added_layer, null, flags)
+ new_turf.assemble_baseturfs(initial(added_layer.baseturfs)) // The baseturfs list is created like roundstart
+ if(!length(new_turf.baseturfs))
+ new_turf.baseturfs = list(baseturfs)
+
+ // The old baseturfs are put underneath, and we sort out the unwanted ones
+ new_turf.baseturfs = baseturfs_string_list(old_baseturfs + (new_turf.baseturfs - GLOB.blacklisted_automated_baseturfs), new_turf)
+ return new_turf
+
/turf/proc/insert_self_into_baseturfs()
baseturfs += type
diff --git a/code/game/turfs/walls/wall_types.dm b/code/game/turfs/walls/wall_types.dm
index abd222f2ba..35a5d53379 100644
--- a/code/game/turfs/walls/wall_types.dm
+++ b/code/game/turfs/walls/wall_types.dm
@@ -712,7 +712,7 @@ INITIALIZE_IMMEDIATE(/turf/closed/wall/indestructible/splashscreen)
var/hivenumber = XENO_HIVE_NORMAL
var/should_track_build = FALSE
var/datum/cause_data/construction_data
- flags_turf = TURF_ORGANIC
+ turf_flags = TURF_ORGANIC
/turf/closed/wall/resin/Initialize(mapload)
. = ..()
diff --git a/code/game/turfs/walls/walls.dm b/code/game/turfs/walls/walls.dm
index 229311325b..ffc81cde57 100644
--- a/code/game/turfs/walls/walls.dm
+++ b/code/game/turfs/walls/walls.dm
@@ -173,6 +173,9 @@
if (acided_hole)
. += SPAN_WARNING("There's a large hole in the wall that could've been caused by some sort of acid.")
+ if(turf_flags & TURF_ORGANIC)
+ return // Skip the part below. 'Organic' walls aren't deconstructable with tools.
+
switch(d_state)
if(WALL_STATE_WELD)
. += SPAN_INFO("The outer plating is intact. A blowtorch should slice it open.")
diff --git a/code/game/world.dm b/code/game/world.dm
index 77d48d3593..c23cce5abb 100644
--- a/code/game/world.dm
+++ b/code/game/world.dm
@@ -137,6 +137,7 @@ var/world_topic_spam_protect_time = world.timeofday
GLOB.world_runtime_log = "[GLOB.log_directory]/runtime.log"
GLOB.round_stats = "[GLOB.log_directory]/round_stats.log"
GLOB.scheduler_stats = "[GLOB.log_directory]/round_scheduler_stats.log"
+ GLOB.mapping_log = "[GLOB.log_directory]/mapping.log"
GLOB.mutator_logs = "[GLOB.log_directory]/mutator_logs.log"
start_log(GLOB.tgui_log)
@@ -146,6 +147,7 @@ var/world_topic_spam_protect_time = world.timeofday
start_log(GLOB.world_runtime_log)
start_log(GLOB.round_stats)
start_log(GLOB.scheduler_stats)
+ start_log(GLOB.mapping_log)
start_log(GLOB.mutator_logs)
if(fexists(GLOB.config_error_log))
@@ -363,9 +365,39 @@ var/datum/BSQL_Connection/connection
/world/proc/on_tickrate_change()
SStimer.reset_buckets()
+/**
+ * Handles incresing the world's maxx var and intializing the new turfs and assigning them to the global area.
+ * If map_load_z_cutoff is passed in, it will only load turfs up to that z level, inclusive.
+ * This is because maploading will handle the turfs it loads itself.
+ */
+/world/proc/increase_max_x(new_maxx, map_load_z_cutoff = maxz)
+ if(new_maxx <= maxx)
+ return
+// var/old_max = world.maxx
+ maxx = new_maxx
+ if(!map_load_z_cutoff)
+ return
+// var/area/global_area = GLOB.areas_by_type[world.area] // We're guaranteed to be touching the global area, so we'll just do this
+// var/list/to_add = block(
+// locate(old_max + 1, 1, 1),
+// locate(maxx, maxy, map_load_z_cutoff))
+// global_area.contained_turfs += to_add
+
+/world/proc/increase_max_y(new_maxy, map_load_z_cutoff = maxz)
+ if(new_maxy <= maxy)
+ return
+// var/old_maxy = maxy
+ maxy = new_maxy
+ if(!map_load_z_cutoff)
+ return
+// var/area/global_area = GLOB.areas_by_type[world.area] // We're guarenteed to be touching the global area, so we'll just do this
+// var/list/to_add = block(
+// locate(1, old_maxy + 1, 1),
+// locate(maxx, maxy, map_load_z_cutoff))
+// global_area.contained_turfs += to_add
+
/world/proc/incrementMaxZ()
maxz++
- //SSmobs.MaxZChanged()
/** For initializing and starting byond-tracy when BYOND_TRACY is defined
* byond-tracy is a useful profiling tool that allows the user to view the CPU usage and execution time of procs as they run.
diff --git a/code/modules/admin/game_master/game_master.dm b/code/modules/admin/game_master/game_master.dm
index 2745287727..874bc14ecf 100644
--- a/code/modules/admin/game_master/game_master.dm
+++ b/code/modules/admin/game_master/game_master.dm
@@ -117,7 +117,7 @@ GLOBAL_VAR_INIT(radio_communication_clarity, 100)
current_submenus = list()
game_master_phone = new(null)
- game_master_phone.AddComponent(/datum/component/phone/virtual, "Game Master", "white", "Company Command", null, PHONE_DO_NOT_DISTURB_ON, list(FACTION_MARINE, FACTION_COLONIST, FACTION_WY), list(FACTION_MARINE, FACTION_COLONIST, FACTION_WY), null, using_client)
+ game_master_phone.AddComponent(/datum/component/phone/virtual, "Game Master", "white", "Company Command", null, PHONE_DND_ON, list(FACTION_MARINE, FACTION_COLONIST, FACTION_WY), list(FACTION_MARINE, FACTION_COLONIST, FACTION_WY), null, using_client)
game_master_client.click_intercept = src
diff --git a/code/modules/admin/holder2.dm b/code/modules/admin/holder2.dm
index e7559f3aa4..c733bdd9e9 100644
--- a/code/modules/admin/holder2.dm
+++ b/code/modules/admin/holder2.dm
@@ -147,16 +147,14 @@ you will have to do something like if(client.admin_holder.rights & R_ADMIN) your
return FALSE
return TRUE
-/// gets the role dependant data for tgui-say
-/datum/admins/proc/get_tgui_say_roles()
- var/roles = list()
- if(check_for_rights(R_ADMIN))
- roles += "Admin"
- if(check_for_rights(R_MOD))
- roles += "Mod"
+/// gets any additional channels for tgui-say (admin & mentor)
+/datum/admins/proc/get_tgui_say_extra_channels()
+ var/extra_channels = list()
+ if(check_for_rights(R_ADMIN) || check_for_rights(R_MOD))
+ extra_channels += ADMIN_CHANNEL
if(check_for_rights(R_MENTOR))
- roles += "Mentor"
- return roles
+ extra_channels += MENTOR_CHANNEL
+ return extra_channels
/datum/proc/CanProcCall(procname)
return TRUE
diff --git a/code/modules/admin/player_panel/player_panel.dm b/code/modules/admin/player_panel/player_panel.dm
index e29ea83ef5..03771e31a8 100644
--- a/code/modules/admin/player_panel/player_panel.dm
+++ b/code/modules/admin/player_panel/player_panel.dm
@@ -498,6 +498,8 @@
.["client_ckey"] = targetClient.ckey
.["client_muted"] = targetClient.prefs.muted
+ .["client_age"] = targetClient.player_data.byond_account_age
+ .["first_join"] = targetClient.player_data.first_join_date
.["client_rank"] = targetClient.admin_holder ? targetClient.admin_holder.rank : "Player"
.["client_muted"] = targetClient.prefs.muted
diff --git a/code/modules/admin/verbs/debug.dm b/code/modules/admin/verbs/debug.dm
index a7ee69469f..ed9827ee32 100644
--- a/code/modules/admin/verbs/debug.dm
+++ b/code/modules/admin/verbs/debug.dm
@@ -130,8 +130,10 @@
var/height
if(istype(SSmapping.z_list[cur_z], /datum/space_level))
var/datum/space_level/cur_level = SSmapping.z_list[cur_z]
- width = cur_level.x_bounds - half_chunk_size + 2
- height = cur_level.y_bounds - half_chunk_size + 2
+ cur_x += cur_level.bounds[MAP_MINX] - 1
+ cur_y += cur_level.bounds[MAP_MINY] - 1
+ width = cur_level.bounds[MAP_MAXX] - cur_level.bounds[MAP_MINX] - half_chunk_size + 1
+ height = cur_level.bounds[MAP_MAXY] - cur_level.bounds[MAP_MINY] - half_chunk_size + 1
else
width = world.maxx - half_chunk_size + 2
height = world.maxy - half_chunk_size + 2
@@ -354,3 +356,20 @@
show_browser(usr, "[str]", "Ticker Count", "tickercount")
+
+#ifdef TESTING
+GLOBAL_LIST_EMPTY(dirty_vars)
+
+/client/proc/see_dirty_varedits()
+ set category = "Debug.Mapping"
+ set name = "Dirty Varedits"
+
+ var/list/dat = list()
+ dat += "Abandon all hope ye who enter here
"
+ for(var/thing in GLOB.dirty_vars)
+ dat += "[thing]
"
+ CHECK_TICK
+ var/datum/browser/popup = new(usr, "dirty_vars", "Dirty Varedits", nwidth = 900, nheight = 750)
+ popup.set_content(dat.Join())
+ popup.open()
+#endif
diff --git a/code/modules/admin/verbs/load_event_level.dm b/code/modules/admin/verbs/load_event_level.dm
index 165506376b..72d004e032 100644
--- a/code/modules/admin/verbs/load_event_level.dm
+++ b/code/modules/admin/verbs/load_event_level.dm
@@ -24,8 +24,6 @@
// Get dims & guesstimate center turf (in practice, current implem means min is always 1)
var/dim_x = boundaries[MAP_MAXX] - boundaries[MAP_MINX] + 1
var/dim_y = boundaries[MAP_MAXY] - boundaries[MAP_MINY] + 1
- var/center_x = boundaries[MAP_MINX] + round(dim_x / 2) // Technically off by 0.5 due to above +1. Whatever
- var/center_y = boundaries[MAP_MINY] + round(dim_y / 2)
var/prompt = alert(C, "Are you SURE you want to load this template as level ? This is SLOW and can freeze server for a bit. Dimensions are: [dim_x] x [dim_y]", "Template Confirm" ,"Yes","Nope!")
if(prompt != "Yes")
@@ -40,6 +38,9 @@
to_chat(C, "Failed to load the template to a Z-Level! Sorry!")
return
+ var/center_x = round(loaded.bounds[MAP_MAXX] / 2) // Technically off by 0.5 due to above +1. Whatever
+ var/center_y = round(loaded.bounds[MAP_MAXY] / 2)
+
// Now notify the staff of the load - this goes in addition to the generic template load game log
message_admins("Successfully loaded template as new Z-Level by ckey: [logckey], template name: [template.name]", center_x, center_y, loaded.z_value)
if(isobserver(C?.mob))
diff --git a/code/modules/admin/view_variables/color_matrix_editor.dm b/code/modules/admin/view_variables/color_matrix_editor.dm
index 078d2fc612..1e58d7fd61 100644
--- a/code/modules/admin/view_variables/color_matrix_editor.dm
+++ b/code/modules/admin/view_variables/color_matrix_editor.dm
@@ -69,6 +69,7 @@ INITIALIZE_IMMEDIATE(/atom/movable/screen/color_matrix_proxy_view)
proxy_view.appearance = image('icons/misc/colortest.dmi', "colors")
proxy_view.color = current_color
+ proxy_view.appearance_flags |= TILE_BOUND
proxy_view.register_to_client(owner)
/datum/color_matrix_editor/Destroy(force, ...)
diff --git a/code/modules/admin/view_variables/filterrific.dm b/code/modules/admin/view_variables/filterrific.dm
index 67190faadb..2383cd614f 100644
--- a/code/modules/admin/view_variables/filterrific.dm
+++ b/code/modules/admin/view_variables/filterrific.dm
@@ -53,7 +53,7 @@
target.change_filter_priority(params["name"], new_priority)
. = TRUE
if("transition_filter_value")
- target.transition_filter(params["name"], 4, params["new_data"])
+ target.transition_filter(params["name"], params["new_data"], 4)
. = TRUE
if("modify_filter_value")
var/list/old_filter_data = target.filter_data[params["name"]]
@@ -69,7 +69,7 @@
if("modify_color_value")
var/new_color = input(usr, "Pick new filter color", "Filteriffic Colors!") as color|null
if(new_color)
- target.transition_filter(params["name"], 4, list("color" = new_color))
+ target.transition_filter(params["name"], list("color" = new_color), 4)
. = TRUE
if("modify_icon_value")
var/icon/new_icon = input("Pick icon:", "Icon") as null|icon
diff --git a/code/modules/admin/view_variables/get_variables.dm b/code/modules/admin/view_variables/get_variables.dm
index a2b87b0909..9ec449e4c3 100644
--- a/code/modules/admin/view_variables/get_variables.dm
+++ b/code/modules/admin/view_variables/get_variables.dm
@@ -121,12 +121,12 @@
switch(.["class"])
if(VV_TEXT)
- .["value"] = tgui_input_text(usr, "Enter new text:", "Text", current_value, encode = FALSE)
+ .["value"] = tgui_input_text(usr, "Enter new text:", "Text", current_value, encode = FALSE, trim = FALSE)
if(.["value"] == null)
.["class"] = null
return
if(VV_MESSAGE)
- .["value"] = tgui_input_text(usr, "Enter new text:", "Text", current_value, encode = FALSE)
+ .["value"] = tgui_input_text(usr, "Enter new text:", "Text", current_value, encode = FALSE, trim = FALSE)
if(.["value"] == null)
.["class"] = null
return
diff --git a/code/modules/asset_cache/assets/tgui.dm b/code/modules/asset_cache/assets/tgui.dm
index 9c79925602..4b31d93e03 100644
--- a/code/modules/asset_cache/assets/tgui.dm
+++ b/code/modules/asset_cache/assets/tgui.dm
@@ -1,3 +1,23 @@
+// If you use a file(...) object, instead of caching the asset it will be loaded from disk every time it's requested.
+// This is useful for development, but not recommended for production.
+// And if TGS is defined, we're being run in a production environment.
+
+#ifdef TGS
+/datum/asset/simple/tgui
+ keep_local_name = FALSE
+ assets = list(
+ "tgui.bundle.js" = "tgui/public/tgui.bundle.js",
+ "tgui.bundle.css" = "tgui/public/tgui.bundle.css",
+ )
+
+/datum/asset/simple/tgui_panel
+ keep_local_name = FALSE
+ assets = list(
+ "tgui-panel.bundle.js" = "tgui/public/tgui-panel.bundle.js",
+ "tgui-panel.bundle.css" = "tgui/public/tgui-panel.bundle.css",
+ )
+
+#else
/datum/asset/simple/tgui
keep_local_name = TRUE
assets = list(
@@ -11,3 +31,5 @@
"tgui-panel.bundle.js" = file("tgui/public/tgui-panel.bundle.js"),
"tgui-panel.bundle.css" = file("tgui/public/tgui-panel.bundle.css"),
)
+
+#endif
diff --git a/code/modules/client/client_procs.dm b/code/modules/client/client_procs.dm
index 5dc0600287..d69833ec5c 100644
--- a/code/modules/client/client_procs.dm
+++ b/code/modules/client/client_procs.dm
@@ -13,7 +13,7 @@ GLOBAL_LIST_INIT(blacklisted_builds, list(
"1548" = "bug breaking the \"alpha\" functionality in the game, allowing clients to be able to see things/mobs they should not be able to see.",
))
-#define LIMITER_SIZE 5
+#define LIMITER_SIZE 12
#define CURRENT_SECOND 1
#define SECOND_COUNT 2
#define CURRENT_MINUTE 3
@@ -55,6 +55,12 @@ GLOBAL_LIST_INIT(whitelisted_client_procs, list(
/client/proc/set_eye_blur_type,
))
+/client/proc/reduce_minute_count()
+ if (!topiclimiter)
+ topiclimiter = new(LIMITER_SIZE)
+ if(topiclimiter[MINUTE_COUNT] > 0)
+ topiclimiter[MINUTE_COUNT] -= 1
+
/client/Topic(href, href_list, hsrc)
if(!usr || usr != mob) //stops us calling Topic for somebody else's client. Also helps prevent usr=null
return
diff --git a/code/modules/client/preferences.dm b/code/modules/client/preferences.dm
index f6aa0a9018..e54d86c7b7 100644
--- a/code/modules/client/preferences.dm
+++ b/code/modules/client/preferences.dm
@@ -245,7 +245,7 @@ var/const/MAX_SAVE_SLOTS = 10
var/auto_observe = TRUE
/datum/preferences/New(client/C)
- key_bindings = deepCopyList(GLOB.hotkey_keybinding_list_by_key) // give them default keybinds and update their movement keys
+ key_bindings = deep_copy_list(GLOB.hotkey_keybinding_list_by_key) // give them default keybinds and update their movement keys
macros = new(C, src)
if(istype(C))
owner = C
diff --git a/code/modules/client/preferences_savefile.dm b/code/modules/client/preferences_savefile.dm
index 1403b64bc4..4e6e166b2b 100644
--- a/code/modules/client/preferences_savefile.dm
+++ b/code/modules/client/preferences_savefile.dm
@@ -168,7 +168,7 @@
/proc/sanitize_keybindings(value)
var/list/base_bindings = sanitize_islist(value, list())
if(!length(base_bindings))
- base_bindings = deepCopyList(GLOB.hotkey_keybinding_list_by_key)
+ base_bindings = deep_copy_list(GLOB.hotkey_keybinding_list_by_key)
for(var/key in base_bindings)
base_bindings[key] = base_bindings[key] & GLOB.keybindings_by_name
if(!length(base_bindings[key]))
diff --git a/code/modules/client/tgui_macro.dm b/code/modules/client/tgui_macro.dm
index f245f1d657..de44b3b523 100644
--- a/code/modules/client/tgui_macro.dm
+++ b/code/modules/client/tgui_macro.dm
@@ -123,7 +123,7 @@ GLOBAL_LIST_EMPTY(ui_data_keybindings)
if(choice == "Cancel")
return TRUE
prefs.hotkeys = (choice == "Hotkey")
- prefs.key_bindings = (prefs.hotkeys) ? deepCopyList(GLOB.hotkey_keybinding_list_by_key) : deepCopyList(GLOB.classic_keybinding_list_by_key)
+ prefs.key_bindings = (prefs.hotkeys) ? deep_copy_list(GLOB.hotkey_keybinding_list_by_key) : deep_copy_list(GLOB.classic_keybinding_list_by_key)
INVOKE_ASYNC(owner, /client/proc/set_macros)
prefs.save_preferences()
return TRUE
diff --git a/code/modules/clothing/suits/marine_armor.dm b/code/modules/clothing/suits/marine_armor.dm
index 93160e68ff..72d0400c7a 100644
--- a/code/modules/clothing/suits/marine_armor.dm
+++ b/code/modules/clothing/suits/marine_armor.dm
@@ -408,7 +408,7 @@
/obj/item/clothing/suit/storage/marine/smartgunner/mob_can_equip(mob/equipping_mob, slot, disable_warning = FALSE)
. = ..()
- if(equipping_mob.back)
+ if(equipping_mob.back && !(equipping_mob.back.flags_item & SMARTGUNNER_BACKPACK_OVERRIDE))
to_chat(equipping_mob, SPAN_WARNING("You can't equip [src] while wearing a backpack."))
return FALSE
@@ -424,10 +424,9 @@
if(slot != WEAR_BACK)
return
-
- if(is_type_in_list(equipping_item, smartgun_back))
+ if(equipping_item.flags_item & SMARTGUNNER_BACKPACK_OVERRIDE || is_type_in_list(equipping_item, smartgun_back))
return
-
+
. = COMPONENT_HUMAN_CANCEL_ATTEMPT_EQUIP
if(equipping_item.flags_equip_slot == SLOT_BACK)
diff --git a/code/modules/cm_marines/dropship_equipment.dm b/code/modules/cm_marines/dropship_equipment.dm
index 955b009fbe..17d612b3ba 100644
--- a/code/modules/cm_marines/dropship_equipment.dm
+++ b/code/modules/cm_marines/dropship_equipment.dm
@@ -6,6 +6,8 @@
icon = 'icons/obj/structures/props/almayer_props.dmi'
climbable = TRUE
layer = ABOVE_OBJ_LAYER //so they always appear above attach points when installed
+ var/shorthand
+
var/list/equip_categories //on what kind of base this can be installed.
var/obj/effect/attach_point/ship_base //the ship base the equipment is currently installed on.
var/uses_ammo = FALSE //whether it uses ammo
@@ -19,10 +21,11 @@
var/skill_required = SKILL_PILOT_TRAINED
var/combat_equipment = TRUE
+
/obj/structure/dropship_equipment/Destroy()
QDEL_NULL(ammo_equipped)
if(linked_shuttle)
- linked_shuttle.equipments -= src
+ SEND_SIGNAL(linked_shuttle, COMSIG_DROPSHIP_REMOVE_EQUIPMENT, src)
linked_shuttle = null
if(ship_base)
ship_base.installed_equipment = null
@@ -33,6 +36,7 @@
linked_console = null
. = ..()
+
/obj/structure/dropship_equipment/attack_alien(mob/living/carbon/xenomorph/current_xenomorph)
if(unslashable)
return XENO_NO_DELAY_ACTION
@@ -122,7 +126,7 @@
ship_base.installed_equipment = null
ship_base = null
if(linked_shuttle)
- linked_shuttle.equipments -= src
+ SEND_SIGNAL(linked_shuttle, COMSIG_DROPSHIP_REMOVE_EQUIPMENT, src)
linked_shuttle = null
if(linked_console && linked_console.selected_equipment == src)
linked_console.selected_equipment = null
@@ -159,7 +163,8 @@
health = null
icon_state = "sentry_system"
is_interactable = TRUE
- point_cost = 500
+ point_cost = 200
+ shorthand = "Sentry"
var/deployment_cooldown
var/obj/structure/machinery/defenses/sentry/premade/dropship/deployed_turret
combat_equipment = FALSE
@@ -176,6 +181,28 @@
if(!deployed_turret)
. += "Its turret is missing."
+/obj/structure/dropship_equipment/sentry_holder/ui_data(mob/user)
+ var/obj/structure/machinery/defenses/defense = deployed_turret
+ . = list()
+ var/is_deployed = deployed_turret.loc != src
+ .["name"] = defense.name
+ .["area"] = get_area(defense)
+ .["active"] = defense.turned_on
+ .["nickname"] = defense.nickname
+ .["camera_available"] = defense.has_camera && is_deployed
+ .["selection_state"] = list()
+ .["kills"] = defense.kills
+ .["iff_status"] = defense.faction_group
+ .["health"] = defense.health
+ .["health_max"] = defense.health_max
+ .["deployed"] = is_deployed
+
+ if(istype(defense, /obj/structure/machinery/defenses/sentry))
+ var/obj/structure/machinery/defenses/sentry/sentrygun = defense
+ .["rounds"] = sentrygun.ammo.current_rounds
+ .["max_rounds"] = sentrygun.ammo.max_rounds
+ .["engaged"] = length(sentrygun.targets)
+
/obj/structure/dropship_equipment/sentry_holder/on_launch()
if(ship_base && ship_base.base_category == DROPSHIP_WEAPON) //only external sentires are automatically undeployed
undeploy_sentry()
@@ -261,8 +288,7 @@
deployed_turret.linked_cam.network = list(CAMERA_NET_ALAMO)
else if (linked_shuttle.id == DROPSHIP_NORMANDY)
deployed_turret.linked_cam.network = list(CAMERA_NET_NORMANDY)
- else if(linked_shuttle.id == DROPSHIP_MIDWAY)
- deployed_turret.linked_cam.network = list(CAMERA_NET_MIDWAY)
+
/obj/structure/dropship_equipment/sentry_holder/proc/undeploy_sentry()
if(!deployed_turret)
@@ -291,6 +317,7 @@
equip_categories = list(DROPSHIP_WEAPON, DROPSHIP_CREW_WEAPON)
icon_state = "mg_system"
point_cost = 50
+ shorthand = "MG"
var/deployment_cooldown
var/obj/structure/machinery/m56d_hmg/mg_turret/dropship/deployed_mg
combat_equipment = FALSE
@@ -301,6 +328,21 @@
deployed_mg = new(src)
deployed_mg.deployment_system = src
+/obj/structure/dropship_equipment/mg_holder/Destroy()
+ QDEL_NULL(deployed_mg)
+ . = ..()
+
+/obj/structure/dropship_equipment/mg_holder/ui_data(mob/user)
+ . = list()
+ var/is_deployed = deployed_mg.loc != src
+ .["name"] = name
+ .["selection_state"] = list()
+ .["health"] = health
+ .["health_max"] = initial(health)
+ .["rounds"] = deployed_mg.rounds
+ .["max_rounds"] = deployed_mg.rounds_max
+ .["deployed"] = is_deployed
+
/obj/structure/dropship_equipment/mg_holder/get_examine_text(mob/user)
. = ..()
if(!deployed_mg)
@@ -331,6 +373,9 @@
..()
+/obj/structure/dropship_equipment/mg_holder/equipment_interact(mob/user)
+ attack_hand(user)
+
/obj/structure/dropship_equipment/mg_holder/update_equipment()
if(ship_base)
setDir(ship_base.dir)
@@ -340,9 +385,10 @@
if(ship_base.base_category == DROPSHIP_WEAPON)
switch(dir)
if(NORTH)
- if( istype(get_step(src, WEST), /turf/open) )
+ var/step_contents = get_step(src, EAST).contents
+ if(locate(/obj/structure) in step_contents)
deployed_mg.pixel_x = 5
- else if ( istype(get_step(src, EAST), /turf/open) )
+ else
deployed_mg.pixel_x = -5
if(EAST)
deployed_mg.pixel_y = 9
@@ -360,7 +406,7 @@
deployed_mg.forceMove(src)
deployed_mg.setDir(dir)
else
- icon_state = "mg_system_destroyed"
+ icon_state = "sentry_system_destroyed"
/obj/structure/dropship_equipment/mg_holder/proc/deploy_mg(mob/user)
if(deployed_mg)
@@ -369,12 +415,11 @@
if(ship_base.base_category == DROPSHIP_WEAPON)
switch(dir)
if(NORTH)
- if( istype(get_step(src, WEST), /turf/open) )
+ var/step_contents = get_step(src, EAST).contents
+ if(locate(/obj/structure) in step_contents)
deployed_mg.forceMove(get_step(src, WEST))
- else if ( istype(get_step(src, EAST), /turf/open) )
- deployed_mg.forceMove(get_step(src, EAST))
else
- deployed_mg.forceMove(get_step(src, NORTH))
+ deployed_mg.forceMove(get_step(src, EAST))
if(EAST)
deployed_mg.forceMove(get_step(src, SOUTH))
if(WEST)
@@ -443,10 +488,9 @@
point_cost = 0
-#define LIGHTING_MAX_LUMINOSITY_SHIPLIGHTS 12
-
/obj/structure/dropship_equipment/electronics/spotlights
name = "\improper AN/LEN-15 Spotlight"
+ shorthand = "Spotlight"
icon_state = "spotlights"
desc = "A set of high-powered spotlights to illuminate large areas. Fits on electronics attach points of dropships. Moving this will require a powerloader."
is_interactable = TRUE
@@ -458,7 +502,7 @@
if(spotlights_cooldown > world.time)
to_chat(user, SPAN_WARNING("[src] is busy."))
return //prevents spamming deployment/undeployment
- if(luminosity != brightness)
+ if(!light_on)
set_light(brightness)
icon_state = "spotlights_on"
to_chat(user, SPAN_NOTICE("You turn on [src]."))
@@ -471,22 +515,22 @@
/obj/structure/dropship_equipment/electronics/spotlights/update_equipment()
..()
if(ship_base)
- if(luminosity != brightness)
+ if(!light_on)
icon_state = "spotlights_off"
else
icon_state = "spotlights_on"
else
icon_state = "spotlights"
- if(luminosity)
+ if(light_on)
set_light(0)
-/obj/structure/dropship_equipment/electronics/spotlights/on_launch()
- set_light(0)
-
-/obj/structure/dropship_equipment/electronics/spotlights/on_arrival()
- set_light(brightness)
-
-#undef LIGHTING_MAX_LUMINOSITY_SHIPLIGHTS
+/obj/structure/dropship_equipment/electronics/spotlights/ui_data(mob/user)
+ . = list()
+ var/is_deployed = light_on
+ .["name"] = name
+ .["health"] = health
+ .["health_max"] = initial(health)
+ .["deployed"] = is_deployed
@@ -497,6 +541,7 @@
/obj/structure/dropship_equipment/electronics/targeting_system
name = "\improper AN/AAQ-178 Weapon Targeting System"
+ shorthand = "Targeting"
icon_state = "targeting_system"
desc = "A targeting system for dropships. It improves firing accuracy on laser targets. Fits on electronics attach points. You need a powerloader to lift this."
point_cost = 800
@@ -509,7 +554,8 @@
/obj/structure/dropship_equipment/electronics/landing_zone_detector
name = "\improper AN/AVD-60 LZ detector"
- desc = "An electronic device linked to the dropship's camera system that lets you observe your landing zone."
+ shorthand = "LZ Detector"
+ desc = "An electronic device linked to the dropship's camera system that lets you observe your landing zone mid-flight."
icon_state = "lz_detector"
point_cost = 50
var/obj/structure/machinery/computer/cameras/dropship/linked_cam_console
@@ -639,7 +685,7 @@
msg_admin_niche("[key_name(user)] is direct-firing [SA] onto [selected_target] at ([target_turf.x],[target_turf.y],[target_turf.z]) [ADMIN_JMP(target_turf)]")
if(ammo_travelling_time)
- var/total_seconds = max(round(ammo_travelling_time/10),1)
+ var/total_seconds = max(floor(ammo_travelling_time/10),1)
for(var/i = 0 to total_seconds)
sleep(10)
if(!selected_target || !selected_target.loc)//if laser disappeared before we reached the target,
@@ -655,7 +701,7 @@
new /obj/effect/overlay/temp/blinking_laser (impact)
sleep(10)
SA.source_mob = user
- SA.detonate_on(impact)
+ SA.detonate_on(impact, src)
/obj/structure/dropship_equipment/weapon/proc/open_fire_firemission(obj/selected_target, mob/user = usr)
set waitfor = 0
@@ -681,7 +727,7 @@
var/turf/impact = pick(possible_turfs)
sleep(3)
SA.source_mob = user
- SA.detonate_on(impact)
+ SA.detonate_on(impact, src)
/obj/structure/dropship_equipment/weapon/heavygun
name = "\improper GAU-21 30mm cannon"
@@ -691,6 +737,7 @@
point_cost = 400
skill_required = SKILL_PILOT_TRAINED
fire_mission_only = FALSE
+ shorthand = "GAU"
/obj/structure/dropship_equipment/weapon/heavygun/update_icon()
if(ammo_equipped)
@@ -707,6 +754,7 @@
firing_sound = 'sound/effects/rocketpod_fire.ogg'
firing_delay = 5
point_cost = 600
+ shorthand = "MSL"
/obj/structure/dropship_equipment/weapon/rocket_pod/deplete_ammo()
ammo_equipped = null //nothing left to empty after firing
@@ -728,6 +776,7 @@
firing_sound = 'sound/effects/rocketpod_fire.ogg'
firing_delay = 10 //1 seconds
point_cost = 600
+ shorthand = "RKT"
/obj/structure/dropship_equipment/weapon/minirocket_pod/update_icon()
if(ammo_equipped && ammo_equipped.ammo_count)
@@ -751,6 +800,7 @@
point_cost = 500
skill_required = SKILL_PILOT_TRAINED
fire_mission_only = FALSE
+ shorthand = "LZR"
/obj/structure/dropship_equipment/weapon/laser_beam_gun/update_icon()
if(ammo_equipped && ammo_equipped.ammo_count)
@@ -768,7 +818,8 @@
firing_delay = 10 //1 seconds
bound_height = 32
equip_categories = list(DROPSHIP_CREW_WEAPON) //fits inside the central spot of the dropship
- point_cost = 400
+ point_cost = 200
+ shorthand = "LCH"
/obj/structure/dropship_equipment/weapon/launch_bay/update_equipment()
if(ship_base)
@@ -782,6 +833,7 @@
/obj/structure/dropship_equipment/medevac_system
name = "\improper RMU-4M Medevac System"
+ shorthand = "Medevac"
desc = "A winch system to lift injured marines on medical stretchers onto the dropship. Acquire lift target through the dropship equipment console."
equip_categories = list(DROPSHIP_CREW_WEAPON)
icon_state = "medevac_system"
@@ -807,24 +859,8 @@
linked_stretcher = null
icon_state = "medevac_system"
-
-/obj/structure/dropship_equipment/medevac_system/equipment_interact(mob/user)
- if(!linked_shuttle)
- return
-
- if(linked_shuttle.mode != SHUTTLE_CALL)
- to_chat(user, SPAN_WARNING("[src] can only be used while in flight."))
- return
-
- if(busy_winch)
- to_chat(user, SPAN_WARNING(" The winch is already in motion."))
- return
-
- if(world.time < medevac_cooldown)
- to_chat(user, SPAN_WARNING("[src] was just used, you need to wait a bit before using it again."))
- return
-
- var/list/possible_stretchers = list()
+/obj/structure/dropship_equipment/medevac_system/proc/get_targets()
+ . = list()
for(var/obj/structure/bed/medevac_stretcher/MS in activated_medevac_stretchers)
var/area/AR = get_area(MS)
var/evaccee_name
@@ -851,20 +887,33 @@
if (evaccee_triagecard_color && evaccee_triagecard_color == "none")
evaccee_triagecard_color = null
- possible_stretchers["[evaccee_name] [evaccee_triagecard_color ? "\[" + uppertext(evaccee_triagecard_color) + "\]" : ""] ([AR.name])"] = MS
+ var/key_name = strip_improper("[evaccee_name] [evaccee_triagecard_color ? "\[" + uppertext(evaccee_triagecard_color) + "\]" : ""] ([AR.name])")
+ .[key_name] = MS
- if(!possible_stretchers.len)
- to_chat(user, SPAN_WARNING("No active medevac stretcher detected."))
- return
+/obj/structure/dropship_equipment/medevac_system/proc/can_medevac(mob/user)
+ if(!linked_shuttle)
+ return FALSE
- var/stretcher_choice = tgui_input_list(usr, "Which emitting stretcher would you like to link with?", "Available stretchers", possible_stretchers)
- if(!stretcher_choice)
- return
+ if(linked_shuttle.mode != SHUTTLE_CALL)
+ to_chat(user, SPAN_WARNING("[src] can only be used while in flight."))
+ return FALSE
- var/obj/structure/bed/medevac_stretcher/selected_stretcher = possible_stretchers[stretcher_choice]
- if(!selected_stretcher)
- return
+ if(busy_winch)
+ to_chat(user, SPAN_WARNING(" The winch is already in motion."))
+ return FALSE
+ if(world.time < medevac_cooldown)
+ to_chat(user, SPAN_WARNING("[src] was just used, you need to wait a bit before using it again."))
+ return FALSE
+
+ var/list/possible_stretchers = get_targets()
+
+ if(!length(possible_stretchers))
+ to_chat(user, SPAN_WARNING("No active medevac stretcher detected."))
+ return FALSE
+ return TRUE
+
+/obj/structure/dropship_equipment/medevac_system/proc/position_dropship(mob/user, obj/structure/bed/medevac_stretcher/selected_stretcher)
if(!ship_base) //system was uninstalled midway
return
@@ -914,6 +963,31 @@
linked_stretcher.linked_medevac = src
linked_stretcher.visible_message(SPAN_NOTICE("[linked_stretcher] detects a dropship overhead."))
+/obj/structure/dropship_equipment/medevac_system/proc/automate_interact(mob/user, stretcher_choice)
+ if(!can_medevac(user))
+ return
+
+ var/list/possible_stretchers = get_targets()
+
+ var/obj/structure/bed/medevac_stretcher/selected_stretcher = possible_stretchers[stretcher_choice]
+ if(!selected_stretcher)
+ return
+ position_dropship(user, selected_stretcher)
+
+/obj/structure/dropship_equipment/medevac_system/equipment_interact(mob/user)
+ if(!can_medevac(user))
+ return
+
+ var/list/possible_stretchers = get_targets()
+
+ var/stretcher_choice = tgui_input_list(usr, "Which emitting stretcher would you like to link with?", "Available stretchers", possible_stretchers)
+ if(!stretcher_choice)
+ return
+
+ var/obj/structure/bed/medevac_stretcher/selected_stretcher = possible_stretchers[stretcher_choice]
+ if(!selected_stretcher)
+ return
+ position_dropship(user, selected_stretcher)
//on arrival we break any link
@@ -959,6 +1033,35 @@
activate_winch(user)
+/obj/structure/dropship_equipment/medevac_system/ui_data(mob/user)
+ var/list/stretchers = get_targets()
+
+ . = list()
+ for(var/stretcher_ref in stretchers)
+ var/obj/structure/bed/medevac_stretcher/stretcher = stretchers[stretcher_ref]
+
+ var/area/AR = get_area(stretcher)
+ var/list/target_data = list()
+ target_data["area"] = AR
+ target_data["ref"] = stretcher_ref
+
+ var/mob/living/carbon/human/occupant = stretcher.buckled_mob
+ if(occupant)
+ target_data["occupant"] = occupant.name
+ target_data["time_of_death"] = occupant.tod
+ target_data["damage"] = list(
+ "hp" = occupant.health,
+ "brute" = occupant.bruteloss,
+ "oxy" = occupant.oxyloss,
+ "tox" = occupant.toxloss,
+ "fire" = occupant.fireloss
+ )
+ if(ishuman(occupant))
+ target_data["damage"]["undefib"] = occupant.undefibbable
+ target_data["triage_card"] = occupant.holo_card_color
+
+ . += list(target_data)
+
/obj/structure/dropship_equipment/medevac_system/proc/activate_winch(mob/user)
set waitfor = 0
var/old_stretcher = linked_stretcher
@@ -1014,6 +1117,7 @@
/obj/structure/dropship_equipment/fulton_system
name = "\improper RMU-19 Fulton Recovery System"
+ shorthand = "Fulton"
desc = "A winch system to collect any fulton recovery balloons in high altitude. Make sure you turn it on!"
equip_categories = list(DROPSHIP_CREW_WEAPON)
icon_state = "fulton_system"
@@ -1030,7 +1134,28 @@
icon_state = "fulton_system"
-/obj/structure/dropship_equipment/fulton_system/equipment_interact(mob/user)
+/obj/structure/dropship_equipment/fulton_system/proc/automate_interact(mob/user, fulton_choice)
+ if(!can_fulton(user))
+ return
+
+ var/list/possible_fultons = get_targets()
+
+ if(!fulton_choice)
+ return
+ // Strip any \proper or \improper in order to match the entry in possible_fultons.
+ fulton_choice = strip_improper(fulton_choice)
+ var/obj/item/stack/fulton/fult = possible_fultons[fulton_choice]
+
+ if(!ship_base) //system was uninstalled midway
+ return
+
+ if(is_ground_level(fult.z)) //in case the fulton popped during our input()
+ return
+
+ if(!fult.attached_atom)
+ to_chat(user, SPAN_WARNING("This balloon stretcher is empty."))
+ return
+
if(!linked_shuttle)
return
@@ -1046,16 +1171,52 @@
to_chat(user, SPAN_WARNING("[src] was just used, you need to wait a bit before using it again."))
return
- var/list/possible_fultons = list()
+ to_chat(user, SPAN_NOTICE(" You move your dropship above the selected balloon's beacon."))
+
+ activate_winch(user, fult)
+
+
+/obj/structure/dropship_equipment/fulton_system/proc/can_fulton(mob/user)
+ if(!linked_shuttle)
+ return FALSE
+
+ if(linked_shuttle.mode != SHUTTLE_CALL)
+ to_chat(user, SPAN_WARNING("[src] can only be used while in flight."))
+ return FALSE
+
+ if(busy_winch)
+ to_chat(user, SPAN_WARNING(" The winch is already in motion."))
+ return FALSE
+
+ if(world.time < fulton_cooldown)
+ to_chat(user, SPAN_WARNING("[src] was just used, you need to wait a bit before using it again."))
+ return FALSE
+ return TRUE
+
+/obj/structure/dropship_equipment/fulton_system/ui_data(mob/user)
+ var/list/targets = get_targets()
+ . = list()
+ for(var/i in targets)
+ . += list(i)
+
+
+/obj/structure/dropship_equipment/fulton_system/proc/get_targets()
+ . = list()
for(var/obj/item/stack/fulton/F in deployed_fultons)
var/recovery_object
if(F.attached_atom)
recovery_object = F.attached_atom.name
else
recovery_object = "Empty"
- possible_fultons["[recovery_object]"] = F
+ .["[recovery_object]"] = F
+
+/obj/structure/dropship_equipment/fulton_system/equipment_interact(mob/user)
+ if(!can_fulton(user))
+ return
- if(!possible_fultons.len)
+ var/list/possible_fultons = get_targets()
+
+ if(!length(possible_fultons))
to_chat(user, SPAN_WARNING("No active balloons detected."))
return
@@ -1129,16 +1290,22 @@
fulton_cooldown = world.time + 50
// Rappel deployment system
-/obj/structure/dropship_equipment/rappel_system
- name = "\improper HPU-1 Rappel Deployment System"
+/obj/structure/dropship_equipment/paradrop_system
+ name = "\improper HPU-1 Paradrop Deployment System"
+ shorthand = "PDS"
equip_categories = list(DROPSHIP_CREW_WEAPON)
icon_state = "rappel_module_packaged"
point_cost = 50
combat_equipment = FALSE
-
+ var/system_cooldown
var/harness = /obj/item/rappel_harness
-/obj/structure/dropship_equipment/rappel_system/update_equipment()
+/obj/structure/dropship_equipment/paradrop_system/ui_data(mob/user)
+ . = list()
+ .["signal"] = "[linked_shuttle.paradrop_signal]"
+ .["locked"] = !!linked_shuttle.paradrop_signal
+
+/obj/structure/dropship_equipment/paradrop_system/update_equipment()
if(ship_base)
icon_state = "rappel_hatch_closed"
density = FALSE
@@ -1148,7 +1315,7 @@
/obj/effect/warning/rappel
color = "#17d17a"
-/obj/structure/dropship_equipment/rappel_system/attack_hand(mob/living/carbon/human/user)
+/obj/structure/dropship_equipment/paradrop_system/attack_hand(mob/living/carbon/human/user)
var/datum/cas_iff_group/cas_group = cas_groups[FACTION_MARINE]
var/list/targets = cas_group.cas_signals
@@ -1227,7 +1394,7 @@
icon_state = "rappel_hatch_closed"
qdel(warning_zone)
-/obj/structure/dropship_equipment/rappel_system/proc/can_use(mob/living/carbon/human/user)
+/obj/structure/dropship_equipment/paradrop_system/proc/can_use(mob/living/carbon/human/user)
if(linked_shuttle.mode != SHUTTLE_CALL)
to_chat(user, SPAN_WARNING("\The [src] can only be used while in flight."))
return FALSE
@@ -1273,4 +1440,4 @@
var/turf/impact = pick(possible_turfs)
sleep(3)
SA.source_mob = user
- SA.detonate_on(impact)
+ SA.detonate_on(impact, src)
diff --git a/code/modules/cm_phone/phone_base.dm b/code/modules/cm_phone/phone_base.dm
index d5f252846b..7c64fd7ee3 100644
--- a/code/modules/cm_phone/phone_base.dm
+++ b/code/modules/cm_phone/phone_base.dm
@@ -11,7 +11,7 @@
var/phone_icon
/// Whether or not the phone is receiving calls or not. Varies between on/off or forcibly on/off.
- var/do_not_disturb = PHONE_DO_NOT_DISTURB_OFF
+ var/do_not_disturb = PHONE_DND_OFF
var/list/networks_receive = list(FACTION_MARINE)
var/list/networks_transmit = list(FACTION_MARINE)
@@ -45,10 +45,10 @@
icon_state = PHONE_ON_BASE_UNIT_ICON_STATE
/obj/structure/phone_base/hidden
- do_not_disturb = PHONE_DO_NOT_DISTURB_FORCED
+ do_not_disturb = PHONE_DND_FORCED
/obj/structure/phone_base/no_dnd
- do_not_disturb = PHONE_DO_NOT_DISTURB_FORBIDDEN
+ do_not_disturb = PHONE_DND_FORBIDDEN
//rotary desk phones (need a touch tone handset at some point)
/obj/structure/phone_base/rotary
@@ -57,7 +57,7 @@
desc = "The finger plate is a little stiff."
/obj/structure/phone_base/rotary/no_dnd
- do_not_disturb = PHONE_DO_NOT_DISTURB_FORBIDDEN
+ do_not_disturb = PHONE_DND_FORBIDDEN
/obj/structure/phone_base/touchtone
name = "touch-tone telephone"
diff --git a/code/modules/droppod/droppod_ui.dm b/code/modules/droppod/droppod_ui.dm
index b0c6683a4f..6493e9ec6c 100644
--- a/code/modules/droppod/droppod_ui.dm
+++ b/code/modules/droppod/droppod_ui.dm
@@ -42,6 +42,7 @@ GLOBAL_LIST_INIT(droppod_target_mode, list(
var/atom/movable/screen/map_view/cam_screen
var/atom/movable/screen/background/cam_background
var/map_name
+ var/list/cam_plane_masters
var/list/ordered_area = list()
var/list/launch_list = list()
@@ -132,17 +133,37 @@ GLOBAL_LIST_INIT(droppod_target_mode, list(
map_name = "admin_supplypod_bay_[REF(src)]_map"
// Initialize map objects
cam_screen = new
+ cam_screen.icon = null
cam_screen.name = "screen"
cam_screen.assigned_map = map_name
cam_screen.del_on_map_removal = TRUE
cam_screen.screen_loc = "[map_name]:1,1"
+ cam_screen.appearance_flags |= TILE_BOUND
cam_background = new
cam_background.assigned_map = map_name
cam_background.del_on_map_removal = TRUE
+ cam_background.appearance_flags |= TILE_BOUND
+
+ cam_plane_masters = list()
+ for(var/plane in subtypesof(/atom/movable/screen/plane_master) - /atom/movable/screen/plane_master/blackness)
+ var/atom/movable/screen/plane_master/instance = new plane()
+ add_plane(instance)
+
refresh_view()
holder.register_map_obj(cam_screen)
holder.register_map_obj(cam_background)
+ for(var/plane_id in cam_plane_masters)
+ holder.register_map_obj(cam_plane_masters[plane_id])
+
+/datum/admin_podlauncher/proc/add_plane(atom/movable/screen/plane_master/instance)
+ instance.assigned_map = map_name
+ instance.appearance_flags |= TILE_BOUND
+ instance.del_on_map_removal = FALSE
+ if(instance.blend_mode_override)
+ instance.blend_mode = instance.blend_mode_override
+ instance.screen_loc = "[map_name]:CENTER"
+ cam_plane_masters["[instance.plane]"] = instance
/datum/admin_podlauncher/proc/refresh_view()
switch(tab_index)
@@ -432,4 +453,5 @@ GLOBAL_LIST_INIT(droppod_target_mode, list(
user.client?.clear_map(map_name)
QDEL_NULL(cam_screen)
QDEL_NULL(cam_background)
+ QDEL_LIST_ASSOC_VAL(cam_plane_masters)
qdel(src)
diff --git a/code/modules/dropships/attach_points/attach_point.dm b/code/modules/dropships/attach_points/attach_point.dm
index 6724f5d18b..cee26f0b13 100644
--- a/code/modules/dropships/attach_points/attach_point.dm
+++ b/code/modules/dropships/attach_points/attach_point.dm
@@ -60,7 +60,7 @@
for(var/obj/docking_port/mobile/marine_dropship/shuttle in SSshuttle.mobile)
if(shuttle.id == ship_tag)
SE.linked_shuttle = shuttle
- shuttle.equipments += SE
+ SEND_SIGNAL(shuttle, COMSIG_DROPSHIP_ADD_EQUIPMENT, SE)
break
SE.update_equipment()
diff --git a/code/modules/dropships/cas/fire_mission_record.dm b/code/modules/dropships/cas/fire_mission_record.dm
index 093a1509b2..2887ec92c2 100644
--- a/code/modules/dropships/cas/fire_mission_record.dm
+++ b/code/modules/dropships/cas/fire_mission_record.dm
@@ -5,6 +5,11 @@
/// List of transverse offsets for each firing step - null meaning not shooting
var/list/offsets
+/datum/cas_fire_mission_record/ui_data(mob/user)
+ . = list()
+ .["weapon"] = weapon.ship_base.attach_id
+ .["offsets"] = offsets
+
/// Get offset range allowed when firing weapon in this configuration
/datum/cas_fire_mission_record/proc/get_offsets()
if(!weapon?.ship_base)
diff --git a/code/modules/lighting/lighting_static/static_lighting_turf.dm b/code/modules/lighting/lighting_static/static_lighting_turf.dm
index 2fe918fa88..ec91e17e4f 100644
--- a/code/modules/lighting/lighting_static/static_lighting_turf.dm
+++ b/code/modules/lighting/lighting_static/static_lighting_turf.dm
@@ -31,7 +31,8 @@
return !(luminosity || dynamic_lumcount)
-/turf/proc/change_area(area/old_area, area/new_area)
+///Transfer the lighting of one area to another
+/turf/proc/transfer_area_lighting(area/old_area, area/new_area)
if(SSlighting.initialized)
if (new_area.static_lighting != old_area.static_lighting)
if (new_area.static_lighting)
@@ -44,6 +45,8 @@
if(new_area.lighting_effect)
overlays += new_area.lighting_effect
+
+
/turf/proc/static_generate_missing_corners()
if (!lighting_corner_NE)
lighting_corner_NE = new/datum/static_lighting_corner(src, NORTH|EAST)
diff --git a/code/modules/logging/global_logs.dm b/code/modules/logging/global_logs.dm
index ff384fe537..f8a3a99c87 100644
--- a/code/modules/logging/global_logs.dm
+++ b/code/modules/logging/global_logs.dm
@@ -28,6 +28,9 @@ GLOBAL_PROTECT(scheduler_stats)
GLOBAL_VAR(mutator_logs)
GLOBAL_PROTECT(mutator_logs)
+GLOBAL_VAR(mapping_log)
+GLOBAL_PROTECT(mapping_log)
+
GLOBAL_VAR(round_stats)
GLOBAL_PROTECT(round_stats)
diff --git a/code/modules/mapping/map_template.dm b/code/modules/mapping/map_template.dm
index 35f7d30a97..50a343635d 100644
--- a/code/modules/mapping/map_template.dm
+++ b/code/modules/mapping/map_template.dm
@@ -7,6 +7,20 @@
var/datum/parsed_map/cached_map
var/keep_cached_map = FALSE
+ ///Default area associated with the map template
+ var/default_area
+
+ ///if true, turfs loaded from this template are placed on top of the turfs already there, defaults to TRUE
+ var/should_place_on_top = TRUE
+
+ ///if true, creates a list of all atoms created by this template loading, defaults to FALSE
+ var/returns_created_atoms = FALSE
+
+ ///the list of atoms created by this template being loaded, only populated if returns_created_atoms is TRUE
+ var/list/created_atoms = list()
+ //make sure this list is accounted for/cleared if you request it from ssatoms!
+
+
/datum/map_template/New(path = null, rename = null, cache = FALSE)
if(path)
mappath = path
@@ -25,71 +39,129 @@
cached_map = parsed
return bounds
-/datum/parsed_map/proc/initTemplateBounds()
- var/list/obj/structure/cable/cables = list()
- var/list/atom/atoms = list()
- var/list/area/areas = list()
+/datum/map_template/proc/initTemplateBounds(list/bounds)
+ if (!bounds) //something went wrong
+ stack_trace("[name] template failed to initialize correctly!")
+ return
- var/list/turfs = block( locate(bounds[MAP_MINX], bounds[MAP_MINY], bounds[MAP_MINZ]),
- locate(bounds[MAP_MAXX], bounds[MAP_MAXY], bounds[MAP_MAXZ]))
- for(var/L in turfs)
- var/turf/B = L
- atoms += B
- areas |= B.loc
- for(var/A in B)
- atoms += A
- if(istype(A, /obj/structure/cable))
- cables += A
- continue
+ var/list/atom/movable/movables = list()
+ var/list/obj/docking_port/stationary/ports = list()
+ var/list/area/areas = list()
+ var/list/turfs = block(
+ locate(
+ bounds[MAP_MINX],
+ bounds[MAP_MINY],
+ bounds[MAP_MINZ]
+ ),
+ locate(
+ bounds[MAP_MAXX],
+ bounds[MAP_MAXY],
+ bounds[MAP_MAXZ]
+ )
+ )
+ for(var/turf/current_turf as anything in turfs)
+ var/area/current_turfs_area = current_turf.loc
+ areas |= current_turfs_area
+ if(!SSatoms.initialized)
+ continue
+
+ for(var/movable_in_turf in current_turf)
+ if(istype(movable_in_turf, /obj/docking_port/mobile))
+ continue // mobile docking ports need to be initialized after their template has finished loading, to ensure that their bounds are setup
+ movables += movable_in_turf
+ if(istype(movable_in_turf, /obj/docking_port/stationary))
+ ports += movable_in_turf
+
+ // Not sure if there is some importance here to make sure the area is in z
+ // first or not. Its defined In Initialize yet its run first in templates
+ // BEFORE so... hummm
SSmapping.reg_in_areas_in_z(areas)
- SSatoms.InitializeAtoms(atoms)
- //SSmachines.setup_template_powernets(cables) // mapping TODO:
-
-/datum/map_template/proc/load_new_z()
- var/x = round((world.maxx - width)/2)
- var/y = round((world.maxy - height)/2)
+ if(!SSatoms.initialized)
+ return
- var/datum/space_level/level = SSmapping.add_new_zlevel(name, list(ZTRAIT_AWAY = TRUE))
- var/datum/parsed_map/parsed = load_map(file(mappath), x, y, level.z_value, no_changeturf=(SSatoms.initialized == INITIALIZATION_INSSATOMS), placeOnTop=TRUE)
+ SSatoms.InitializeAtoms(areas + turfs + movables, returns_created_atoms ? created_atoms : null)
+
+ for(var/turf/unlit as anything in turfs)
+ var/area/loc_area = unlit.loc
+ if(!loc_area.static_lighting)
+ continue
+ unlit.static_lighting_build_overlay()
+
+/datum/map_template/proc/load_new_z(secret = FALSE)
+ var/x = round((world.maxx - width) * 0.5) + 1
+ var/y = round((world.maxy - height) * 0.5) + 1
+
+ var/datum/space_level/level = SSmapping.add_new_zlevel(name, list(), contain_turfs = FALSE)
+ var/datum/parsed_map/parsed = load_map(
+ file(mappath),
+ x,
+ y,
+ level.z_value,
+ no_changeturf = (SSatoms.initialized == INITIALIZATION_INSSATOMS),
+ place_on_top = should_place_on_top,
+ new_z = TRUE,
+ )
var/list/bounds = parsed.bounds
if(!bounds)
return FALSE
repopulate_sorted_areas()
-
//initialize things that are normally initialized after map load
- parsed.initTemplateBounds()
- log_game("Z-level [name] loaded at at [x],[y],[world.maxz]")
+ initTemplateBounds(bounds)
+ log_game("Z-level [name] loaded at [x],[y],[world.maxz]")
return level
-/datum/map_template/proc/load(turf/T, centered, delete)
+/datum/map_template/proc/load(turf/T, centered = FALSE, delete = FALSE)
if(centered)
T = locate(T.x - round(width/2) , T.y - round(height/2) , T.z)
if(!T)
return
- if(T.x + width > world.maxx)
+ if((T.x+width) - 1 > world.maxx)
return
- if(T.y + height > world.maxy)
+ if((T.y+height) - 1 > world.maxy)
return
// Accept cached maps, but don't save them automatically - we don't want
// ruins clogging up memory for the whole round.
var/datum/parsed_map/parsed = cached_map || new(file(mappath))
cached_map = keep_cached_map ? parsed : null
- if(!parsed.load(T.x, T.y, T.z, cropMap = TRUE, no_changeturf = (SSatoms.initialized == INITIALIZATION_INSSATOMS), placeOnTop = TRUE, delete = delete))
+
+ var/list/turf_blacklist = list()
+ update_blacklist(T, turf_blacklist)
+
+ UNSETEMPTY(turf_blacklist)
+ parsed.turf_blacklist = turf_blacklist
+ if(!parsed.load(
+ T.x,
+ T.y,
+ T.z,
+ crop_map = TRUE,
+ no_changeturf = (SSatoms.initialized == INITIALIZATION_INSSATOMS),
+ place_on_top = should_place_on_top,
+ delete = delete
+ ))
return
+
var/list/bounds = parsed.bounds
if(!bounds)
return
+ repopulate_sorted_areas()
+
//initialize things that are normally initialized after map load
- parsed.initTemplateBounds()
+ initTemplateBounds(bounds)
- log_game("[name] loaded at at [T.x],[T.y],[T.z]")
+ log_game("[name] loaded at [T.x],[T.y],[T.z]")
return bounds
+/datum/map_template/proc/post_load()
+ return
+
+/datum/map_template/proc/update_blacklist(turf/T, list/input_blacklist)
+ return
+
/datum/map_template/proc/get_affected_turfs(turf/T, centered = FALSE)
var/turf/placement = T
if(centered)
diff --git a/code/modules/mapping/preloader.dm b/code/modules/mapping/preloader.dm
index e8eee898a7..bd89d0e030 100644
--- a/code/modules/mapping/preloader.dm
+++ b/code/modules/mapping/preloader.dm
@@ -1,33 +1,42 @@
// global datum that will preload variables on atoms instanciation
GLOBAL_VAR_INIT(use_preloader, FALSE)
-GLOBAL_DATUM_INIT(_preloader, /datum/map_preloader, new)
+GLOBAL_LIST_INIT(_preloader_attributes, null)
+GLOBAL_LIST_INIT(_preloader_path, null)
/// Preloader datum
/datum/map_preloader
- parent_type = /datum
var/list/attributes
var/target_path
-/datum/map_preloader/proc/setup(list/the_attributes, path)
+/world/proc/preloader_setup(list/the_attributes, path)
if(the_attributes.len)
GLOB.use_preloader = TRUE
- attributes = the_attributes
- target_path = path
+ GLOB._preloader_attributes = the_attributes
+ GLOB._preloader_path = path
-/datum/map_preloader/proc/load(atom/what)
+/world/proc/preloader_load(atom/what)
GLOB.use_preloader = FALSE
+ var/list/attributes = GLOB._preloader_attributes
for(var/attribute in attributes)
var/value = attributes[attribute]
if(islist(value))
- value = deepCopyList(value)
+ value = deep_copy_list(value)
+ #ifdef TESTING
+ if(what.vars[attribute] == value)
+ var/message = "[what.type] at [AREACOORD(what)] - VAR: [attribute] = [isnull(value) ? "null" : (isnum(value) ? value : "\"[value]\"")]"
+ log_mapping("DIRTY VAR: [message]")
+ GLOB.dirty_vars += message
+ #endif
what.vars[attribute] = value
-/// Area passthrough: do not instanciate a new area, reuse the current one
+/// Template noop (no operation) is used to skip a turf or area when the template is loaded this allows for template transparency
+/// ex. if a ship has gaps in it's design, you would use template_noop to fill these in so that when the ship moves z-level, any
+/// tiles these gaps land on will not be deleted and replaced with the ships (empty) tiles
/area/template_noop
name = "Area Passthrough"
icon_state = "noop"
-/// Turf passthrough: do not instanciate a new turf, reuse the current one
+/// See above explanation
/turf/template_noop
name = "Turf Passthrough"
icon_state = "noop"
diff --git a/code/modules/mapping/reader.dm b/code/modules/mapping/reader.dm
index 969dc6a795..424ab22ae4 100644
--- a/code/modules/mapping/reader.dm
+++ b/code/modules/mapping/reader.dm
@@ -1,7 +1,67 @@
///////////////////////////////////////////////////////////////
//SS13 Optimized Map loader
//////////////////////////////////////////////////////////////
-#define SPACE_KEY "space"
+// We support two different map formats
+// It is kinda possible to process them together, but if we split them up
+// I can make optimization decisions more easily
+/**
+ * DMM SPEC:
+ * DMM is split into two parts. First we have strings of text linked to lists of paths and their modifications (I will call this the cache)
+ * We call these strings "keys" and the things they point to members. Keys have a static length
+ *
+ * The second part is a list of locations matched to a string of keys. (I'll be calling this the grid)
+ * These are used to lookup the cache we built earlier.
+ * We store location lists as grid_sets. the lines represent different things depending on the spec
+ *
+ * In standard DMM (which you can treat as the base case, since it also covers weird modifications) each line
+ * represents an x file, and there's typically only one grid set per z level.
+ * The meme is you can look at a DMM formatted map and literally see what it should roughly look like
+ * This differs in TGM, and we can pull some performance from this
+ *
+ * Any restrictions here also apply to TGM
+ *
+ * /tg/ Restrictions:
+ * Paths have a specified order. First atoms in the order in which they should be loaded, then a single turf, then the area of the cell
+ * DMM technically supports turf stacking, but this is deprecated for all formats
+
+ */
+#define MAP_DMM "dmm"
+/**
+ * TGM SPEC:
+ * TGM is a derevation of DMM, with restrictions placed on it
+ * to make it easier to parse and to reduce merge conflicts/ease their resolution
+ *
+ * Requirements:
+ * Each "statement" in a key's details ends with a new line, and wrapped in (...)
+ * All paths end with either a comma or occasionally a {, then a new line
+ * Excepting the area, who is listed last and ends with a ) to mark the end of the key
+ *
+ * {} denotes a list of variable edits applied to the path that came before the first {
+ * the final } is followed by a comma, and then a new line
+ * Variable edits have the form \tname = value;\n
+ * Except the last edit, which has no final ;, and just ends in a newline
+ * No extra padding is permitted
+ * Many values are supported. See parse_constant()
+ * Strings must be wrapped in "...", files in '...', and lists in list(...)
+ * Files are kinda susy, and may not actually work. buyer beware
+ * Lists support assoc values as expected
+ * These constants can be further embedded into lists
+ *
+ * There can be no padding in front of, or behind a path
+ *
+ * Therefore:
+ * "key" = (
+ * /path,
+ * /other/path{
+ * var = list("name" = 'filepath');
+ * other_var = /path
+ * },
+ * /turf,
+ * /area)
+ *
+ */
+#define MAP_TGM "tgm"
+#define MAP_UNKNOWN "unknown"
/datum/grid_set
var/xcrd
@@ -11,9 +71,20 @@
/datum/parsed_map
var/original_path
+ var/map_format
+ /// The length of a key in this file. This is promised by the standard to be static
var/key_len = 0
+ /// The length of a line in this file. Not promised by dmm but standard dmm uses it, so we can trust it
+ var/line_len = 0
+ /// If we've expanded world.maxx
+ var/expanded_y = FALSE
+ /// If we've expanded world.maxy
+ var/expanded_x = FALSE
var/list/grid_models = list()
var/list/gridSets = list()
+ /// List of area types we've loaded AS A PART OF THIS MAP
+ /// We do this to allow non unique areas, so we'll only load one per map
+ var/list/area/loaded_areas = list()
var/list/modelCache
@@ -22,33 +93,97 @@
/// Offset bounds. Same as parsed_bounds until load().
var/list/bounds
+ ///any turf in this list is skipped inside of build_coordinate. Lazy assoc list
+ var/list/turf_blacklist
+
// raw strings used to represent regexes more accurately
// '' used to avoid confusing syntax highlighting
- var/static/regex/dmmRegex = new(@'"([a-zA-Z]+)" = \(((?:.|\n)*?)\)\n(?!\t)|\((\d+),(\d+),(\d+)\) = \{"([a-zA-Z\n]*)"\}', "g")
- var/static/regex/trimQuotesRegex = new(@'^[\s\n]+"?|"?[\s\n]+$|^"|"$', "g")
- var/static/regex/trimRegex = new(@'^[\s\n]+|[\s\n]+$', "g")
+ var/static/regex/dmm_regex = new(@'"([a-zA-Z]+)" = (?:\(\n|\()((?:.|\n)*?)\)\n(?!\t)|\((\d+),(\d+),(\d+)\) = \{"([a-zA-Z\n]*)"\}', "g")
+ /// Matches key formats in TMG (IE: newline after the \()
+ var/static/regex/matches_tgm = new(@'^"[A-z]*"[\s]*=[\s]*\([\s]*\n', "m")
+ /// Pulls out key value pairs for TGM
+ var/static/regex/var_edits_tgm = new(@'^\t([A-z]*) = (.*?);?$')
+ /// Pulls out model paths for DMM
+ var/static/regex/model_path = new(@'(\/[^\{]*?(?:\{.*?\})?)(?:,|$)', "g")
+
+ /// If we are currently loading this map
+ var/loading = FALSE
#ifdef TESTING
var/turfsSkipped = 0
#endif
-/// Shortcut function to parse a map and apply it to the world.
-///
-/// - `dmm_file`: A .dmm file to load (Required).
-/// - `x_offset`, `y_offset`, `z_offset`: Positions representign where to load the map (Optional).
-/// - `cropMap`: When true, the map will be cropped to fit the existing world dimensions (Optional).
-/// - `measureOnly`: When true, no changes will be made to the world (Optional).
-/// - `no_changeturf`: When true, [turf/AfterChange] won't be called on loaded turfs
-/// - `x_lower`, `x_upper`, `y_lower`, `y_upper`: Coordinates (relative to the map) to crop to (Optional).
-/// - `placeOnTop`: Whether to use [turf/PlaceOnTop] rather than [turf/ChangeTurf] (Optional).
-/proc/load_map(dmm_file as file, x_offset as num, y_offset as num, z_offset as num, cropMap as num, measureOnly as num, no_changeturf as num, x_lower = -INFINITY as num, x_upper = INFINITY as num, y_lower = -INFINITY as num, y_upper = INFINITY as num, placeOnTop = FALSE as num)
- var/datum/parsed_map/parsed = new(dmm_file, x_lower, x_upper, y_lower, y_upper, measureOnly)
- if(parsed.bounds && !measureOnly)
- parsed.load(x_offset, y_offset, z_offset, cropMap, no_changeturf, x_lower, x_upper, y_lower, y_upper, placeOnTop)
- return parsed
+/datum/parsed_map/proc/copy()
+ // Avoids duped work just in case
+ build_cache()
+ var/datum/parsed_map/newfriend = new()
+ newfriend.original_path = original_path
+ newfriend.map_format = map_format
+ newfriend.key_len = key_len
+ newfriend.line_len = line_len
+ newfriend.grid_models = grid_models.Copy()
+ newfriend.gridSets = gridSets.Copy()
+ newfriend.modelCache = modelCache.Copy()
+ newfriend.parsed_bounds = parsed_bounds.Copy()
+ // Copy parsed bounds to reset to initial values
+ newfriend.bounds = parsed_bounds.Copy()
+ newfriend.turf_blacklist = turf_blacklist?.Copy()
+ return newfriend
+
+//text trimming (both directions) helper macro
+#define TRIM_TEXT(text) (trim_reduced(text))
+
+/**
+ * Helper and recommened way to load a map file
+ * - dmm_file: The path to the map file
+ * - x_offset: The x offset to load the map at
+ * - y_offset: The y offset to load the map at
+ * - z_offset: The z offset to load the map at
+ * - crop_map: If true, the map will be cropped to the world bounds
+ * - measure_only: If true, the map will not be loaded, but the bounds will be calculated
+ * - no_changeturf: If true, the map will not call /turf/AfterChange
+ * - x_lower: The minimum x coordinate to load
+ * - x_upper: The maximum x coordinate to load
+ * - y_lower: The minimum y coordinate to load
+ * - y_upper: The maximum y coordinate to load
+ * - z_lower: The minimum z coordinate to load
+ * - z_upper: The maximum z coordinate to load
+ * - place_on_top: Whether to use /turf/proc/PlaceOnTop rather than /turf/proc/ChangeTurf
+ * - new_z: If true, a new z level will be created for the map
+ * - delete: CM/TGMC addition, if we need to manually clear turf contents before spawning stuff
+ */
+/proc/load_map(
+ dmm_file,
+ x_offset = 0,
+ y_offset = 0,
+ z_offset = 0,
+ crop_map = FALSE,
+ measure_only = FALSE,
+ no_changeturf = FALSE,
+ x_lower = -INFINITY,
+ x_upper = INFINITY,
+ y_lower = -INFINITY,
+ y_upper = INFINITY,
+ z_lower = -INFINITY,
+ z_upper = INFINITY,
+ place_on_top = FALSE,
+ new_z = FALSE,
+ delete = FALSE,
+)
+ if(!(dmm_file in GLOB.cached_maps))
+ GLOB.cached_maps[dmm_file] = new /datum/parsed_map(dmm_file)
+
+ var/datum/parsed_map/parsed_map = GLOB.cached_maps[dmm_file]
+ parsed_map = parsed_map.copy()
+ if(!measure_only && !isnull(parsed_map.bounds))
+ parsed_map.load(x_offset, y_offset, z_offset, crop_map, no_changeturf, x_lower, x_upper, y_lower, y_upper, z_lower, z_upper, place_on_top, new_z, delete)
+ return parsed_map
/// Parse a map, possibly cropping it.
-/datum/parsed_map/New(tfile, x_lower = -INFINITY, x_upper = INFINITY, y_lower = -INFINITY, y_upper=INFINITY, measureOnly=FALSE)
+/datum/parsed_map/New(tfile, x_lower = -INFINITY, x_upper = INFINITY, y_lower = -INFINITY, y_upper=INFINITY, z_lower = -INFINITY, z_upper=INFINITY, measureOnly=FALSE)
+ // This proc sleeps for like 6 seconds. why?
+ // Is it file accesses? if so, can those be done ahead of time, async to save on time here? I wonder.
+ // Love ya :)
if(isfile(tfile))
original_path = "[tfile]"
tfile = file2text(tfile)
@@ -56,16 +191,30 @@
// create a new datum without loading a map
return
- bounds = parsed_bounds = list(1.#INF, 1.#INF, 1.#INF, -1.#INF, -1.#INF, -1.#INF)
- var/stored_index = 1
+ src.bounds = parsed_bounds = list(1.#INF, 1.#INF, 1.#INF, -1.#INF, -1.#INF, -1.#INF)
+
+ if(findtext(tfile, matches_tgm))
+ map_format = MAP_TGM
+ else
+ map_format = MAP_DMM // Fallback
+
+ // lists are structs don't you know :)
+ var/list/bounds = src.bounds
+ var/list/grid_models = src.grid_models
+ var/key_len = src.key_len
+ var/line_len = src.line_len
+ var/stored_index = 1
+ var/list/regexOutput
//multiz lool
- while(dmmRegex.Find(tfile, stored_index))
- stored_index = dmmRegex.next
+ while(dmm_regex.Find(tfile, stored_index))
+ stored_index = dmm_regex.next
+ // Datum var lookup is expensive, this isn't
+ regexOutput = dmm_regex.group
// "aa" = (/type{vars=blah})
- if(dmmRegex.group[1]) // Model
- var/key = dmmRegex.group[1]
+ if(regexOutput[1]) // Model
+ var/key = regexOutput[1]
if(grid_models[key]) // Duplicate model keys are ignored in DMMs
continue
if(key_len != length(key))
@@ -74,328 +223,776 @@
else
CRASH("Inconsistent key length in DMM")
if(!measureOnly)
- grid_models[key] = dmmRegex.group[2]
+ grid_models[key] = regexOutput[2]
// (1,1,1) = {"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}
- else if(dmmRegex.group[3]) // Coords
+ else if(regexOutput[3]) // Coords
if(!key_len)
CRASH("Coords before model definition in DMM")
- var/curr_x = text2num(dmmRegex.group[3])
-
+ var/curr_x = text2num(regexOutput[3])
if(curr_x < x_lower || curr_x > x_upper)
continue
+ var/curr_y = text2num(regexOutput[4])
+ if(curr_y < y_lower || curr_y > y_upper)
+ continue
+
+ var/curr_z = text2num(regexOutput[5])
+ if(curr_z < z_lower || curr_z > z_upper)
+ continue
+
var/datum/grid_set/gridSet = new
gridSet.xcrd = curr_x
- //position of the currently processed square
- gridSet.ycrd = text2num(dmmRegex.group[4])
- gridSet.zcrd = text2num(dmmRegex.group[5])
+ gridSet.ycrd = curr_y
+ gridSet.zcrd = curr_z
- bounds[MAP_MINX] = min(bounds[MAP_MINX], clamp(gridSet.xcrd, x_lower, x_upper))
- bounds[MAP_MINZ] = min(bounds[MAP_MINZ], gridSet.zcrd)
- bounds[MAP_MAXZ] = max(bounds[MAP_MAXZ], gridSet.zcrd)
+ bounds[MAP_MINX] = min(bounds[MAP_MINX], curr_x)
+ bounds[MAP_MINZ] = min(bounds[MAP_MINZ], curr_y)
+ bounds[MAP_MAXZ] = max(bounds[MAP_MAXZ], curr_z)
- var/list/gridLines = splittext(dmmRegex.group[6], "\n")
+ var/list/gridLines = splittext(regexOutput[6], "\n")
gridSet.gridLines = gridLines
var/leadingBlanks = 0
- while(leadingBlanks < gridLines.len && gridLines[++leadingBlanks] == "")
+ while(leadingBlanks < length(gridLines) && gridLines[++leadingBlanks] == "")
if(leadingBlanks > 1)
gridLines.Cut(1, leadingBlanks) // Remove all leading blank lines.
- if(!gridLines.len) // Skip it if only blank lines exist.
+ if(!length(gridLines)) // Skip it if only blank lines exist.
continue
gridSets += gridSet
- if(gridLines.len && gridLines[gridLines.len] == "")
- gridLines.Cut(gridLines.len) // Remove only one blank line at the end.
+ if(gridLines[length(gridLines)] == "")
+ gridLines.Cut(length(gridLines)) // Remove only one blank line at the end.
+
+ bounds[MAP_MINY] = min(bounds[MAP_MINY], gridSet.ycrd)
+ gridSet.ycrd += length(gridLines) - 1 // Start at the top and work down
+ bounds[MAP_MAXY] = max(bounds[MAP_MAXY], gridSet.ycrd)
- bounds[MAP_MINY] = min(bounds[MAP_MINY], clamp(gridSet.ycrd, y_lower, y_upper))
- gridSet.ycrd += gridLines.len - 1 // Start at the top and work down
- bounds[MAP_MAXY] = max(bounds[MAP_MAXY], clamp(gridSet.ycrd, y_lower, y_upper))
+ if(!line_len)
+ line_len = length(gridLines[1])
- var/maxx = gridSet.xcrd
- if(gridLines.len) //Not an empty map
- maxx = max(maxx, gridSet.xcrd + length(gridLines[1]) / key_len - 1)
+ var/maxx = curr_x
+ if(length(gridLines)) //Not an empty map
+ maxx = max(maxx, curr_x + line_len / key_len - 1)
- bounds[MAP_MAXX] = clamp(max(bounds[MAP_MAXX], maxx), x_lower, x_upper)
+ bounds[MAP_MAXX] = max(bounds[MAP_MAXX], maxx)
CHECK_TICK
// Indicate failure to parse any coordinates by nulling bounds
if(bounds[1] == 1.#INF)
- bounds = null
- parsed_bounds = bounds
+ src.bounds = null
+ else
+ // Clamp all our mins and maxes down to the proscribed limits
+ bounds[MAP_MINX] = clamp(bounds[MAP_MINX], x_lower, x_upper)
+ bounds[MAP_MAXX] = clamp(bounds[MAP_MAXX], x_lower, x_upper)
+ bounds[MAP_MINY] = clamp(bounds[MAP_MINY], y_lower, y_upper)
+ bounds[MAP_MAXY] = clamp(bounds[MAP_MAXY], y_lower, y_upper)
+ bounds[MAP_MINZ] = clamp(bounds[MAP_MINZ], z_lower, z_upper)
+ bounds[MAP_MAXZ] = clamp(bounds[MAP_MAXZ], z_lower, z_upper)
+
+ parsed_bounds = src.bounds
+ src.key_len = key_len
+ src.line_len = line_len
+
+/// Iterates over all grid sets and returns ones with z values within the given bounds. Inclusive
+/datum/parsed_map/proc/filter_grid_sets_based_on_z_bounds(lower_z, upper_z)
+ var/list/filtered_sets = list()
+ for(var/datum/grid_set/grid_set as anything in gridSets)
+ if(grid_set.zcrd < lower_z)
+ continue
+ if(grid_set.zcrd > upper_z)
+ continue
+ filtered_sets += grid_set
+ return filtered_sets
-/// Load the parsed map into the world. See [/proc/load_map] for arguments.
-/datum/parsed_map/proc/load(x_offset, y_offset, z_offset, cropMap, no_changeturf, x_lower, x_upper, y_lower, y_upper, placeOnTop, delete)
+/// Load the parsed map into the world. You probably want [/proc/load_map]. Keep the signature the same.
+/datum/parsed_map/proc/load(x_offset = 0, y_offset = 0, z_offset = 0, crop_map = FALSE, no_changeturf = FALSE, x_lower = -INFINITY, x_upper = INFINITY, y_lower = -INFINITY, y_upper = INFINITY, z_lower = -INFINITY, z_upper = INFINITY, place_on_top = FALSE, new_z = FALSE, delete = FALSE)
//How I wish for RAII
Master.StartLoadingMap()
- . = _load_impl(x_offset, y_offset, z_offset, cropMap, no_changeturf, x_lower, x_upper, y_lower, y_upper, placeOnTop, delete)
+ . = _load_impl(x_offset, y_offset, z_offset, crop_map, no_changeturf, x_lower, x_upper, y_lower, y_upper, z_lower, z_upper, place_on_top, new_z, delete)
Master.StopLoadingMap()
+#define MAPLOADING_CHECK_TICK \
+ if(TICK_CHECK) { \
+ if(loading) { \
+ SSatoms.map_loader_stop(REF(src)); \
+ stoplag(); \
+ SSatoms.map_loader_begin(REF(src)); \
+ } else { \
+ stoplag(); \
+ } \
+ }
+
// Do not call except via load() above.
-/datum/parsed_map/proc/_load_impl(x_offset = 1, y_offset = 1, z_offset = world.maxz + 1, cropMap = FALSE, no_changeturf = FALSE, x_lower = -INFINITY, x_upper = INFINITY, y_lower = -INFINITY, y_upper = INFINITY, placeOnTop = FALSE, delete = FALSE)
- var/list/areaCache = list()
+/datum/parsed_map/proc/_load_impl(x_offset, y_offset, z_offset, crop_map, no_changeturf, x_lower, x_upper, y_lower, y_upper, z_lower, z_upper, place_on_top, new_z, delete)
+ PRIVATE_PROC(TRUE)
+ // Tell ss atoms that we're doing maploading
+ // We'll have to account for this in the following tick_checks so it doesn't overflow
+ loading = TRUE
+ SSatoms.map_loader_begin(REF(src))
+
+ // Loading used to be done in this proc
+ // We make the assumption that if the inner procs runtime, we WANT to do cleanup on them, but we should stil tell our parents we failed
+ // Since well, we did
+ var/sucessful = FALSE
+ switch(map_format)
+ if(MAP_TGM)
+ sucessful = _tgm_load(x_offset, y_offset, z_offset, crop_map, no_changeturf, x_lower, x_upper, y_lower, y_upper, z_lower, z_upper, place_on_top, new_z, delete)
+ else
+ sucessful = _dmm_load(x_offset, y_offset, z_offset, crop_map, no_changeturf, x_lower, x_upper, y_lower, y_upper, z_lower, z_upper, place_on_top, new_z, delete)
+
+ // And we are done lads, call it off
+ SSatoms.map_loader_stop(REF(src))
+ loading = FALSE
+
+ // CM: Disabled due to not using contained_turfs and SSarea_contents
+// if(new_z)
+// for(var/z_index in bounds[MAP_MINZ] to bounds[MAP_MAXZ])
+// SSmapping.build_area_turfs(z_index)
+
+ if(!no_changeturf)
+ var/list/turfs = block(
+ locate(bounds[MAP_MINX], bounds[MAP_MINY], bounds[MAP_MINZ]),
+ locate(bounds[MAP_MAXX], bounds[MAP_MAXY], bounds[MAP_MAXZ]))
+ for(var/turf/T as anything in turfs)
+ //we do this after we load everything in. if we don't, we'll have weird atmos bugs regarding atmos adjacent turfs
+ T.AfterChange(CHANGETURF_IGNORE_AIR)
+
+ if(expanded_x || expanded_y)
+ SEND_GLOBAL_SIGNAL(COMSIG_GLOB_EXPANDED_WORLD_BOUNDS, expanded_x, expanded_y)
+
+ #ifdef TESTING
+ if(turfsSkipped)
+ testing("Skipped loading [turfsSkipped] default turfs")
+ #endif
+
+ return sucessful
+
+// Wanna clear something up about maps, talking in 255x255 here
+// In the tgm format, each gridset contains 255 lines, each line representing one tile, with 255 total gridsets
+// In the dmm format, each gridset contains 255 lines, each line representing one row of tiles, containing 255 * line length characters, with one gridset per z
+// You can think of dmm as storing maps in rows, whereas tgm stores them in columns
+/datum/parsed_map/proc/_tgm_load(x_offset, y_offset, z_offset, crop_map, no_changeturf, x_lower, x_upper, y_lower, y_upper, z_lower, z_upper, place_on_top, new_z, delete)
+ // setup
var/list/modelCache = build_cache(no_changeturf)
var/space_key = modelCache[SPACE_KEY]
var/list/bounds
src.bounds = bounds = list(1.#INF, 1.#INF, 1.#INF, -1.#INF, -1.#INF, -1.#INF)
- for(var/I in gridSets)
- var/datum/grid_set/gset = I
- var/ycrd = gset.ycrd + y_offset - 1
- var/zcrd = gset.zcrd + z_offset - 1
- if(!cropMap && ycrd > world.maxy)
- world.maxy = ycrd // Expand Y here. X is expanded in the loop below
+ // Building y coordinate ranges
+ var/y_relative_to_absolute = y_offset - 1
+ var/x_relative_to_absolute = x_offset - 1
+
+ // Ok so like. something important
+ // We talk in "relative" coords here, so the coordinate system of the map datum
+ // This is so we can do offsets, but it is NOT the same as positions in game
+ // That's why there's some uses of - y_relative_to_absolute here, to turn absolute positions into relative ones
+ // TGM maps process in columns, so the starting y will always be the max size
+ // We know y starts at 1
+ var/datum/grid_set/first_column = gridSets[1]
+ var/relative_y = first_column.ycrd
+ var/highest_y = relative_y + y_relative_to_absolute
+
+ if(!crop_map && highest_y > world.maxy)
+ if(new_z)
+ // Need to avoid improperly loaded area/turf_contents
+ world.increase_max_y(highest_y, map_load_z_cutoff = z_offset - 1)
+ else
+ world.increase_max_y(highest_y)
+ expanded_y = TRUE
+
+ // Skip Y coords that are above the smallest of the three params
+ // So maxy and y_upper get to act as thresholds, and relative_y can play
+ var/y_skip_above = min(world.maxy - y_relative_to_absolute, y_upper, relative_y)
+ // How many lines to skip because they'd be above the y cuttoff line
+ var/y_starting_skip = relative_y - y_skip_above
+ highest_y -= y_starting_skip
+
+ // Y is the LOWEST it will ever be here, so we can easily set a threshold for how low to go
+ var/line_count = length(first_column.gridLines)
+ var/lowest_y = relative_y - (line_count - 1) // -1 because we decrement at the end of the loop, not the start
+ var/y_ending_skip = max(max(y_lower, 1 - y_relative_to_absolute) - lowest_y, 0)
+
+ // X setup
+ var/x_delta_with = x_upper
+ if(crop_map)
+ // Take our smaller crop threshold yes?
+ x_delta_with = min(x_delta_with, world.maxx)
+
+ // We're gonna skip all the entries above the upper x, or maxx if cropMap is set
+ // The last column is guarenteed to have the highest x value we;ll encounter
+ // Even if z scales, this still works
+ var/datum/grid_set/last_column = gridSets[length(gridSets)]
+ var/final_x = last_column.xcrd + x_relative_to_absolute
+
+ if(final_x > x_delta_with)
+ // If our relative x is greater then X upper, well then we've gotta limit our expansion
+ var/delta = max(final_x - x_delta_with, 0)
+ final_x -= delta
+ if(final_x > world.maxx && !crop_map)
+ if(new_z)
+ // Need to avoid improperly loaded area/turf_contents
+ world.increase_max_x(final_x, map_load_z_cutoff = z_offset - 1)
+ else
+ world.increase_max_x(final_x)
+ expanded_x = TRUE
+
+ var/lowest_x = max(x_lower, 1 - x_relative_to_absolute)
+
+ // Amount we offset the grid zcrd to get the true zcrd
+ var/grid_z_offset = z_offset - 1
+ var/z_upper_set = z_upper < INFINITY
+ var/z_lower_set = z_lower > -INFINITY
+
+ // We make the assumption that the last block of turfs will have the highest embedded z in it
+ // true max zcrd
+ var/map_bounds_z_max = last_column.zcrd
+ var/z_upper_parsed = map_bounds_z_max + z_offset - 1
+ if(z_upper_set)
+ z_upper_parsed -= map_bounds_z_max - z_upper
+ if(z_lower_set)
+ var/offset_amount = z_lower - 1
+ z_upper_parsed -= offset_amount
+ grid_z_offset -= offset_amount
+
+ var/list/target_grid_sets = gridSets
+ if(z_lower_set || z_upper_set) // bounds are set, filter out gridsets for z levels we don't want
+ target_grid_sets = filter_grid_sets_based_on_z_bounds(z_lower, z_upper)
+
+ var/z_threshold = world.maxz
+ if(z_upper_parsed > z_threshold && crop_map)
+ for(var/i in z_threshold + 1 to z_upper_parsed) //create a new z_level if needed
+ world.incrementMaxZ()
+ if(!no_changeturf)
+ WARNING("Z-level expansion occurred without no_changeturf set, this may cause problems when /turf/AfterChange is called")
+
+ for(var/datum/grid_set/gset as anything in target_grid_sets)
+ var/true_xcrd = gset.xcrd + x_relative_to_absolute
+
+ // any cutoff of x means we just shouldn't iterate this gridset
+ if(final_x < true_xcrd || lowest_x > gset.xcrd)
+ continue
+
+ var/zcrd = gset.zcrd + grid_z_offset
+ // If we're using changeturf, we disable it if we load into a z level we JUST created
+ var/no_afterchange = no_changeturf || zcrd > z_threshold
+
+ // We're gonna track the first and last pairs of coords we find
+ // Since x is always incremented in steps of 1, we only need to deal in y
+ // The first x is guarenteed to be the lowest, the first y the highest, and vis versa
+ // This is faster then doing mins and maxes inside the hot loop below
+ var/first_found = FALSE
+ var/first_y = 0
+ var/last_y = 0
+
+ var/ycrd = highest_y
+ // Everything following this line is VERY hot.
+ for(var/i in 1 + y_starting_skip to line_count - y_ending_skip)
+ if(gset.gridLines[i] == space_key && no_afterchange)
+ #ifdef TESTING
+ ++turfsSkipped
+ #endif
+ ycrd--
+ MAPLOADING_CHECK_TICK
+ continue
+
+ var/list/cache = modelCache[gset.gridLines[i]]
+ if(!cache)
+ SSatoms.map_loader_stop(REF(src))
+ CRASH("Undefined model key in DMM: [gset.gridLines[i]]")
+ build_coordinate(cache, locate(true_xcrd, ycrd, zcrd), no_afterchange, place_on_top, new_z, delete)
+
+ // only bother with bounds that actually exist
+ if(!first_found)
+ first_found = TRUE
+ first_y = ycrd
+ last_y = ycrd
+ ycrd--
+ MAPLOADING_CHECK_TICK
+
+ // The x coord never changes, so not tracking first x is safe
+ // If no ycrd is found, we assume this row is totally empty and just continue on
+ if(first_found)
+ bounds[MAP_MINX] = min(bounds[MAP_MINX], true_xcrd)
+ bounds[MAP_MINY] = min(bounds[MAP_MINY], last_y)
+ bounds[MAP_MINZ] = min(bounds[MAP_MINZ], zcrd)
+ bounds[MAP_MAXX] = max(bounds[MAP_MAXX], true_xcrd)
+ bounds[MAP_MAXY] = max(bounds[MAP_MAXY], first_y)
+ bounds[MAP_MAXZ] = max(bounds[MAP_MAXZ], zcrd)
+ return TRUE
+
+/// Stanrdard loading, not used in production
+/// Doesn't take advantage of any tgm optimizations, which makes it slower but also more general
+/// Use this if for some reason your map format is messy
+/datum/parsed_map/proc/_dmm_load(x_offset, y_offset, z_offset, crop_map, no_changeturf, x_lower, x_upper, y_lower, y_upper, z_lower, z_upper, place_on_top, new_z, delete)
+ // setup
+ var/list/modelCache = build_cache(no_changeturf)
+ var/space_key = modelCache[SPACE_KEY]
+ var/list/bounds
+ var/key_len = src.key_len
+ src.bounds = bounds = list(1.#INF, 1.#INF, 1.#INF, -1.#INF, -1.#INF, -1.#INF)
+
+ var/y_relative_to_absolute = y_offset - 1
+ var/x_relative_to_absolute = x_offset - 1
+ var/line_len = src.line_len
+
+ // Amount we offset the grid zcrd to get the true zcrd
+ var/grid_z_offset = z_offset - 1
+ var/z_upper_set = z_upper < INFINITY
+ var/z_lower_set = z_lower > -INFINITY
+
+ // we now need to find the maximum z, fun!
+ var/map_bounds_z_max = 1
+ for(var/datum/grid_set/grid_set as anything in gridSets)
+ map_bounds_z_max = max(map_bounds_z_max, grid_set.zcrd)
+
+ var/z_upper_parsed = map_bounds_z_max + z_offset - 1
+ if(z_upper_set)
+ z_upper_parsed -= map_bounds_z_max - z_upper
+ if(z_lower_set)
+ var/offset_amount = z_lower - 1
+ z_upper_parsed -= offset_amount
+ grid_z_offset -= offset_amount
+
+ var/list/target_grid_sets = gridSets
+ if(z_lower_set || z_upper_set) // bounds are set, filter out gridsets for z levels we don't want
+ target_grid_sets = filter_grid_sets_based_on_z_bounds(z_lower, z_upper)
+
+ for(var/datum/grid_set/gset as anything in target_grid_sets)
+ var/relative_x = gset.xcrd
+ var/relative_y = gset.ycrd
+ var/true_xcrd = relative_x + x_relative_to_absolute
+ var/ycrd = relative_y + y_relative_to_absolute
+ var/zcrd = gset.zcrd + grid_z_offset
+ if(!crop_map && ycrd > world.maxy)
+ if(new_z)
+ // Need to avoid improperly loaded area/turf_contents
+ world.increase_max_y(ycrd, map_load_z_cutoff = z_offset - 1)
+ else
+ world.increase_max_y(ycrd)
+ expanded_y = TRUE
var/zexpansion = zcrd > world.maxz
+ var/no_afterchange = no_changeturf
if(zexpansion)
- if(cropMap)
+ if(crop_map)
continue
else
while (zcrd > world.maxz) //create a new z_level if needed
world.incrementMaxZ()
if(!no_changeturf)
WARNING("Z-level expansion occurred without no_changeturf set, this may cause problems when /turf/AfterChange is called")
-
- for(var/line in gset.gridLines)
- if((ycrd - y_offset + 1) < y_lower || (ycrd - y_offset + 1) > y_upper) //Reverse operation and check if it is out of bounds of cropping.
- --ycrd
- continue
- if(ycrd <= world.maxy && ycrd >= 1)
- var/xcrd = gset.xcrd + x_offset - 1
- for(var/tpos = 1 to length(line) - key_len + 1 step key_len)
- if((xcrd - x_offset + 1) < x_lower || (xcrd - x_offset + 1) > x_upper) //Same as above.
- ++xcrd
- continue //X cropping.
- if(xcrd > world.maxx)
- if(cropMap)
- break
- else
- world.maxx = xcrd
-
- if(xcrd >= 1)
- var/model_key = copytext(line, tpos, tpos + key_len)
- var/no_afterchange = no_changeturf || zexpansion
- if(!no_afterchange || (model_key != space_key))
- var/list/cache = modelCache[model_key]
- if(!cache)
- CRASH("Undefined model key in DMM: [model_key]")
- build_coordinate(areaCache, cache, locate(xcrd, ycrd, zcrd), no_afterchange, placeOnTop, delete)
-
- // only bother with bounds that actually exist
- bounds[MAP_MINX] = min(bounds[MAP_MINX], xcrd)
- bounds[MAP_MINY] = min(bounds[MAP_MINY], ycrd)
- bounds[MAP_MINZ] = min(bounds[MAP_MINZ], zcrd)
- bounds[MAP_MAXX] = max(bounds[MAP_MAXX], xcrd)
- bounds[MAP_MAXY] = max(bounds[MAP_MAXY], ycrd)
- bounds[MAP_MAXZ] = max(bounds[MAP_MAXZ], zcrd)
- #ifdef TESTING
- else
- ++turfsSkipped
- #endif
- CHECK_TICK
+ no_afterchange = TRUE
+ // Ok so like. something important
+ // We talk in "relative" coords here, so the coordinate system of the map datum
+ // This is so we can do offsets, but it is NOT the same as positions in game
+ // That's why there's some uses of - y_relative_to_absolute here, to turn absolute positions into relative ones
+
+ // Skip Y coords that are above the smallest of the three params
+ // So maxy and y_upper get to act as thresholds, and relative_y can play
+ var/y_skip_above = min(world.maxy - y_relative_to_absolute, y_upper, relative_y)
+ // How many lines to skip because they'd be above the y cuttoff line
+ var/y_starting_skip = relative_y - y_skip_above
+ ycrd += y_starting_skip
+
+ // Y is the LOWEST it will ever be here, so we can easily set a threshold for how low to go
+ var/line_count = length(gset.gridLines)
+ var/lowest_y = relative_y - (line_count - 1) // -1 because we decrement at the end of the loop, not the start
+ var/y_ending_skip = max(max(y_lower, 1 - y_relative_to_absolute) - lowest_y, 0)
+
+ // Now we're gonna precompute the x thresholds
+ // We skip all the entries below the lower x, or 1
+ var/starting_x_delta = max(max(x_lower, 1 - x_relative_to_absolute) - relative_x, 0)
+ // The x loop counts by key length, so we gotta multiply here
+ var/x_starting_skip = starting_x_delta * key_len
+ true_xcrd += starting_x_delta
+
+ // We're gonna skip all the entries above the upper x, or maxx if cropMap is set
+ var/x_target = line_len - key_len + 1
+ var/x_step_count = ROUND_UP(x_target / key_len)
+ var/final_x = relative_x + (x_step_count - 1)
+ var/x_delta_with = x_upper
+ if(crop_map)
+ // Take our smaller crop threshold yes?
+ x_delta_with = min(x_delta_with, world.maxx)
+ if(final_x > x_delta_with)
+ // If our relative x is greater then X upper, well then we've gotta limit our expansion
+ var/delta = max(final_x - x_delta_with, 0)
+ x_step_count -= delta
+ final_x -= delta
+ x_target = x_step_count * key_len
+ if(final_x > world.maxx && !crop_map)
+ if(new_z)
+ // Need to avoid improperly loaded area/turf_contents
+ world.increase_max_x(final_x, map_load_z_cutoff = z_offset - 1)
+ else
+ world.increase_max_x(final_x)
+ expanded_x = TRUE
+
+ // We're gonna track the first and last pairs of coords we find
+ // The first x is guarenteed to be the lowest, the first y the highest, and vis versa
+ // This is faster then doing mins and maxes inside the hot loop below
+ var/first_found = FALSE
+ var/first_x = 0
+ var/first_y = 0
+ var/last_x = 0
+ var/last_y = 0
+
+ // Everything following this line is VERY hot. How hot depends on the map format
+ // (Yes this does mean dmm is technically faster to parse. shut up)
+ for(var/i in 1 + y_starting_skip to line_count - y_ending_skip)
+ var/line = gset.gridLines[i]
+
+ var/xcrd = true_xcrd
+ for(var/tpos in 1 + x_starting_skip to x_target step key_len)
+ var/model_key = copytext(line, tpos, tpos + key_len)
+ if(model_key == space_key && no_afterchange)
+ #ifdef TESTING
+ ++turfsSkipped
+ #endif
+ MAPLOADING_CHECK_TICK
++xcrd
- --ycrd
-
- CHECK_TICK
+ continue
+ var/list/cache = modelCache[model_key]
+ if(!cache)
+ SSatoms.map_loader_stop(REF(src))
+ CRASH("Undefined model key in DMM: [model_key]")
+ build_coordinate(cache, locate(xcrd, ycrd, zcrd), no_afterchange, place_on_top, new_z, delete)
+
+ // only bother with bounds that actually exist
+ if(!first_found)
+ first_found = TRUE
+ first_x = xcrd
+ first_y = ycrd
+ last_x = xcrd
+ last_y = ycrd
+ MAPLOADING_CHECK_TICK
+ ++xcrd
+ ycrd--
+ MAPLOADING_CHECK_TICK
+ bounds[MAP_MINX] = min(bounds[MAP_MINX], first_x)
+ bounds[MAP_MINY] = min(bounds[MAP_MINY], last_y)
+ bounds[MAP_MINZ] = min(bounds[MAP_MINZ], zcrd)
+ bounds[MAP_MAXX] = max(bounds[MAP_MAXX], last_x)
+ bounds[MAP_MAXY] = max(bounds[MAP_MAXY], first_y)
+ bounds[MAP_MAXZ] = max(bounds[MAP_MAXZ], zcrd)
- //if(!no_changeturf)// mapping TODO:
- // for(var/t in block(locate(bounds[MAP_MINX], bounds[MAP_MINY], bounds[MAP_MINZ]), locate(bounds[MAP_MAXX], bounds[MAP_MAXY], bounds[MAP_MAXZ])))
- // var/turf/T = t
- // //we do this after we load everything in. if we don't; we'll have weird atmos bugs regarding atmos adjacent turfs
- // T.AfterChange(CHANGETURF_IGNORE_AIR)
+ return TRUE
- #ifdef TESTING
- if(turfsSkipped)
- testing("Skipped loading [turfsSkipped] default turfs")
- #endif
+GLOBAL_LIST_EMPTY(map_model_default)
- return TRUE
+/datum/parsed_map/proc/build_cache(no_changeturf, bad_paths)
+ if(map_format == MAP_TGM)
+ return tgm_build_cache(no_changeturf, bad_paths)
+ return dmm_build_cache(no_changeturf, bad_paths)
-/datum/parsed_map/proc/build_cache(no_changeturf, bad_paths=null)
+/datum/parsed_map/proc/tgm_build_cache(no_changeturf, bad_paths=null)
if(modelCache && !bad_paths)
return modelCache
. = modelCache = list()
var/list/grid_models = src.grid_models
+ var/set_space = FALSE
+ // Use where a list is needed, but where it will not be modified
+ // Used here to remove the cost of needing to make a new list for each fields entry when it's set manually later
+ var/static/list/default_list = GLOB.map_model_default // It's stupid, but it saves += list(list)
+ var/static/list/wrapped_default_list = list(default_list) // It's stupid, but it saves += list(list)
+ var/static/regex/var_edits = var_edits_tgm
+
+ var/path_to_init = ""
+ // Reference to the attributes list we're currently filling, if any
+ var/list/current_attributes
+ // If we are currently editing a path or not
+ var/editing = FALSE
for(var/model_key in grid_models)
- var/model = grid_models[model_key]
- var/list/members = list() //will contain all members (paths) in model (in our example : /turf/unsimulated/wall and /area/mine/explored)
- var/list/members_attributes = list() //will contain lists filled with corresponding variables, if any (in our example : list(icon_state = "rock") and list())
+ // We're going to split models by newline
+ // This guarentees that each entry will be of interest to us
+ // Then we'll process them step by step
+ // Hopefully this reduces the cost from read_list that we'd otherwise have
+ var/list/lines = splittext(grid_models[model_key], "\n")
+ // Builds list of path/edits for later
+ // Of note: we cannot preallocate them to save time in list expansion later
+ // But fortunately lists allocate at least 8 entries normally anyway, and
+ // We are unlikely to have more then that many members
+ //will contain all members (paths) in model (in our example : /turf/unsimulated/wall)
+ var/list/members = list()
+ //will contain lists filled with corresponding variables, if any (in our example : list(icon_state = "rock") and list())
+ var/list/members_attributes = list()
/////////////////////////////////////////////////////////
//Constructing members and corresponding variables lists
////////////////////////////////////////////////////////
+ // string representation of the path to init
+ for(var/line in lines)
+ // We do this here to avoid needing to check at each return statement
+ // No harm in it anyway
+ MAPLOADING_CHECK_TICK
+
+ switch(line[length(line)])
+ if(";") // Var edit, we'll apply it
+ // Var edits look like \tname = value;
+ // I'm gonna try capturing them with regex, since it ought to be the fastest here
+ // Should hand back key = value
+ var_edits.Find(line)
+ var/value = parse_constant(var_edits.group[2])
+ if(istext(value))
+ value = apply_text_macros(value)
+ current_attributes[var_edits.group[1]] = value
+ continue // Keep on keeping on brother
+ if("{") // Start of an edit, and so also the start of a path
+ editing = TRUE
+ current_attributes = list() // Init the list we'll be filling
+ members_attributes += list(current_attributes)
+ path_to_init = copytext(line, 1, -1)
+ if(",") // Either the end of a path, or the end of an edit
+ if(editing) // it was the end of a path
+ editing = FALSE
+ continue
+ members_attributes += wrapped_default_list // We know this is a path, and we also know it has no vv's. so we'll just set this to the default list
+ // Drop the last char mind
+ path_to_init = copytext(line, 1, -1)
+ if("}") // Gotta be the end of an area edit, let's check to be sure
+ if(editing) // it was the end of an area edit (shouldn't do those anyhow)
+ editing = FALSE
+ continue
+ stack_trace("ended a line on JUST a }, with no ongoing edit. What? Area shit?")
+ else // If we're editing, this is a var edit entry. the last one in a stack, cause god hates me. Otherwise, it's an area
+ if(editing) // I want inline I want inline I want inline
+ // Var edits look like \tname = value;
+ // I'm gonna try capturing them with regex, since it ought to be the fastest here
+ // Should hand back key = value
+ var_edits.Find(line)
+ var/value = parse_constant(var_edits.group[2])
+ if(istext(value))
+ value = apply_text_macros(value)
+ current_attributes[var_edits.group[1]] = value
+ continue // Keep on keeping on brother
- var/index = 1
- var/old_position = 1
- var/dpos
+ members_attributes += wrapped_default_list // We know this is a path, and we also know it has no vv's. so we'll just set this to the default list
+ path_to_init = line
- while(dpos != 0)
- //finding next member (e.g /turf/unsimulated/wall{icon_state = "rock"} or /area/mine/explored)
- dpos = find_next_delimiter_position(model, old_position, ",", "{", "}") //find next delimiter (comma here) that's not within {...}
- var/full_def = trim_text(copytext(model, old_position, dpos)) //full definition, e.g : /obj/foo/bar{variables=derp}
- var/variables_start = findtext(full_def, "{")
- var/path_text = trim_text(copytext(full_def, 1, variables_start))
+ // Alright, if we've gotten to this point, our string is a path
+ // Oh and we don't trim it, because we require no padding for these
+ // Saves like 1.5 deciseconds
+ var/atom_def = text2path(path_to_init) //path definition, e.g /obj/foo/bar
+
+ if(!ispath(atom_def, /atom)) // Skip the item if the path does not exist. Fix your crap, mappers!
+ if(bad_paths)
+ // Rare case, avoid the var to save time most of the time
+ LAZYOR(bad_paths[copytext(line, 1, -1)], model_key)
+ continue
+ // Index is already incremented either way, just gotta set the path and all
+ members += atom_def
+
+ //check and see if we can just skip this turf
+ //So you don't have to understand this horrid statement, we can do this if
+ // 1. the space_key isn't set yet
+ // 2. no_changeturf is set
+ // 3. there are exactly 2 members
+ // 4. with no attributes
+ // 5. and the members are world.turf and world.area
+ // Basically, if we find an entry like this: "XXX" = (/turf/default, /area/default)
+ // We can skip calling this proc every time we see XXX
+ if(!set_space \
+ && no_changeturf \
+ && members_attributes.len == 2 \
+ && members.len == 2 \
+ && members_attributes[1] == default_list \
+ && members_attributes[2] == default_list \
+ && members[2] == world.area \
+ && members[1] == world.turf
+ )
+ set_space = TRUE
+ .[SPACE_KEY] = model_key
+ continue
+
+ .[model_key] = list(members, members_attributes)
+ return .
+
+/// Builds key caches for general formats
+/// Slower then the proc above, tho it could still be optimized slightly. it's just not a priority
+/// Since we don't run DMM maps, ever.
+/datum/parsed_map/proc/dmm_build_cache(no_changeturf, bad_paths=null)
+ if(modelCache && !bad_paths)
+ return modelCache
+ . = modelCache = list()
+ var/list/grid_models = src.grid_models
+ var/set_space = FALSE
+ // Use where a list is needed, but where it will not be modified
+ // Used here to remove the cost of needing to make a new list for each fields entry when it's set manually later
+ var/static/list/default_list = list(GLOB.map_model_default)
+ for(var/model_key in grid_models)
+ //will contain all members (paths) in model (in our example : /turf/unsimulated/wall)
+ var/list/members = list()
+ //will contain lists filled with corresponding variables, if any (in our example : list(icon_state = "rock") and list())
+ var/list/members_attributes = list()
+
+ var/model = grid_models[model_key]
+ /////////////////////////////////////////////////////////
+ //Constructing members and corresponding variables lists
+ ////////////////////////////////////////////////////////
+
+ var/model_index = 1
+ while(model_path.Find(model, model_index))
+ var/variables_start = 0
+ var/member_string = model_path.group[1]
+ model_index = model_path.next
+ //findtext is a bit expensive, lets only do this if the last char of our string is a } (IE: we know we have vars)
+ //this saves about 25 miliseconds on my machine. Not a major optimization
+ if(member_string[length(member_string)] == "}")
+ variables_start = findtext(member_string, "{")
+
+ var/path_text = TRIM_TEXT(copytext(member_string, 1, variables_start))
var/atom_def = text2path(path_text) //path definition, e.g /obj/foo/bar
- if(dpos)
- old_position = dpos + length(model[dpos])
if(!ispath(atom_def, /atom)) // Skip the item if the path does not exist. Fix your crap, mappers!
if(bad_paths)
LAZYOR(bad_paths[path_text], model_key)
continue
- members.Add(atom_def)
+ members += atom_def
//transform the variables in text format into a list (e.g {var1="derp"; var2; var3=7} => list(var1="derp", var2, var3=7))
- var/list/fields = list()
-
+ // OF NOTE: this could be made faster by replacing readlist with a progressive regex
+ // I'm just too much of a bum to do it rn, especially since we mandate tgm format for any maps in repo
+ var/list/fields = default_list
if(variables_start)//if there's any variable
- full_def = copytext(full_def, variables_start + length(full_def[variables_start]), -length(copytext_char(full_def, -1))) //removing the last '}'
- fields = readlist(full_def, ";")
- if(fields.len)
- if(!trim(fields[fields.len]))
- --fields.len
- for(var/I in fields)
- var/value = fields[I]
- if(istext(value))
- fields[I] = apply_text_macros(value)
+ member_string = copytext(member_string, variables_start + length(member_string[variables_start]), -length(copytext_char(member_string, -1))) //removing the last '}'
+ fields = list(readlist(member_string, ";"))
+ for(var/I in fields)
+ var/value = fields[I]
+ if(istext(value))
+ fields[I] = apply_text_macros(value)
//then fill the members_attributes list with the corresponding variables
- members_attributes.len++
- members_attributes[index++] = fields
-
- CHECK_TICK
+ members_attributes += fields
+ MAPLOADING_CHECK_TICK
//check and see if we can just skip this turf
//So you don't have to understand this horrid statement, we can do this if
- // 1. no_changeturf is set
- // 2. the space_key isn't set yet
+ // 1. the space_key isn't set yet
+ // 2. no_changeturf is set
// 3. there are exactly 2 members
// 4. with no attributes
// 5. and the members are world.turf and world.area
// Basically, if we find an entry like this: "XXX" = (/turf/default, /area/default)
// We can skip calling this proc every time we see XXX
- if(no_changeturf \
- && !(.[SPACE_KEY]) \
+ if(!set_space \
+ && no_changeturf \
&& members.len == 2 \
&& members_attributes.len == 2 \
&& length(members_attributes[1]) == 0 \
&& length(members_attributes[2]) == 0 \
&& (world.area in members) \
&& (world.turf in members))
-
+ set_space = TRUE
.[SPACE_KEY] = model_key
continue
-
.[model_key] = list(members, members_attributes)
+ return .
-/datum/parsed_map/proc/build_coordinate(list/areaCache, list/model, turf/crds, no_changeturf as num, placeOnTop as num, delete)
+/datum/parsed_map/proc/build_coordinate(list/model, turf/crds, no_changeturf as num, placeOnTop as num, new_z, delete)
+ // If we don't have a turf, nothing we will do next will actually acomplish anything, so just go back
+ // Note, this would actually drop area vvs in the tile, but like, why tho
+ if(!crds)
+ return
var/index
var/list/members = model[1]
var/list/members_attributes = model[2]
+ // We use static lists here because it's cheaper then passing them around
+ var/static/list/default_list = GLOB.map_model_default
////////////////
//Instanciation
////////////////
+ if(turf_blacklist?[crds])
+ return
+
//The next part of the code assumes there's ALWAYS an /area AND a /turf on a given tile
//first instance the /area and remove it from the members list
index = members.len
+ var/area/old_area
if(members[index] != /area/template_noop)
- var/atype = members[index]
- GLOB._preloader.setup(members_attributes[index], atype)//preloader for assigning set variables on atom creation
- var/atom/instance = areaCache[atype]
- if(!instance)
- instance = GLOB.areas_by_type[atype]
- if(!instance)
- instance = new atype(null)
- areaCache[atype] = instance
- if(crds)
- instance.contents.Add(crds)
-
- if(GLOB.use_preloader && instance)
- GLOB._preloader.load(instance)
-
- //then instance the /turf and, if multiple tiles are presents, simulates the DMM underlays piling effect
-
- var/first_turf_index = 1
- while(!ispath(members[first_turf_index], /turf)) //find first /turf object in members
- first_turf_index++
-
- //turn off base new Initialization until the whole thing is loaded
- SSatoms.map_loader_begin()
- //instanciate the first /turf
- var/turf/T
- if(members[first_turf_index] != /turf/template_noop)
- T = instance_atom(members[first_turf_index], members_attributes[first_turf_index], crds,no_changeturf, placeOnTop, delete)
-
- if(T)
- //if others /turf are presents, simulates the underlays piling effect
- index = first_turf_index + 1
- while(index <= members.len - 1) // Last item is an /area
- var/underlay = T.appearance
- T = instance_atom(members[index], members_attributes[index], crds,no_changeturf, placeOnTop, delete)//instance new turf
- T.underlays += underlay
- index++
+ if(members_attributes[index] != default_list)
+ world.preloader_setup(members_attributes[index], members[index])//preloader for assigning set variables on atom creation
+ var/area/area_instance = loaded_areas[members[index]]
+ if(!area_instance)
+ var/area_type = members[index]
+ // If this parsed map doesn't have that area already, we check the global cache
+ area_instance = GLOB.areas_by_type[area_type]
+ // If the global list DOESN'T have this area it's either not a unique area, or it just hasn't been created yet
+ if (!area_instance)
+ area_instance = new area_type(null)
+ if(!area_instance)
+ CRASH("[area_type] failed to be new'd, what'd you do?")
+ loaded_areas[area_type] = area_instance
+
+ if(!new_z)
+ old_area = crds.loc
+// old_area.turfs_to_uncontain += crds
+// area_instance.contained_turfs.Add(crds)
+ area_instance.contents.Add(crds)
+
+ if(GLOB.use_preloader)
+ world.preloader_load(area_instance)
+
+ // Index right before /area is /turf
+ index--
+ var/atom/instance
+ //then instance the /turf
+ //NOTE: this used to place any turfs before the last "underneath" it using .appearance and underlays
+ //We don't actually use this, and all it did was cost cpu, so we don't do this anymore
+ if(members[index] != /turf/template_noop)
+ if(members_attributes[index] != default_list)
+ world.preloader_setup(members_attributes[index], members[index])
+
+ // CM/TGMC addition: delete map contents before inserting if delete truthy
+ if(delete)
+ for(var/atom/turf_atom as anything in crds.GetAllTurfStrictContents())
+ if(isobserver(turf_atom))
+ continue
+ qdel(turf_atom, force = TRUE)
+
+ // Note: we make the assertion that the last path WILL be a turf. if it isn't, this will fail.
+ if(placeOnTop)
+ instance = crds.load_on_top(members[index], CHANGETURF_DEFER_CHANGE | (no_changeturf ? CHANGETURF_SKIP : NONE))
+ else if(no_changeturf)
+ instance = create_atom(members[index], crds)//first preloader pass
+ else
+ instance = crds.ChangeTurf(members[index], null, CHANGETURF_DEFER_CHANGE)
+
+ if(GLOB.use_preloader && instance)//second preloader pass, for those atoms that don't ..() in New()
+ world.preloader_load(instance)
+ // If this isn't template work, we didn't change our turf and we changed area, then we've gotta handle area lighting transfer
+ else if(!no_changeturf && old_area)
+ // Don't do contain/uncontain stuff, this happens a few lines up when the area actally changes
+ crds.on_change_area(old_area, crds.loc)
+ MAPLOADING_CHECK_TICK
//finally instance all remainings objects/mobs
- for(index in 1 to first_turf_index-1)
- instance_atom(members[index], members_attributes[index], crds, no_changeturf, placeOnTop, delete)
- //Restore initialization to the previous value
- SSatoms.map_loader_stop()
+ for(var/atom_index in 1 to index-1)
+ if(members_attributes[atom_index] != default_list)
+ world.preloader_setup(members_attributes[atom_index], members[atom_index])
+
+ // We make the assertion that only /atom s will be in this portion of the code. if that isn't true, this will fail
+ instance = create_atom(members[atom_index], crds)//first preloader pass
+
+ if(GLOB.use_preloader && instance)//second preloader pass, for those atoms that don't ..() in New()
+ world.preloader_load(instance)
+ MAPLOADING_CHECK_TICK
////////////////
//Helpers procs
////////////////
-//Instance an atom at (x,y,z) and gives it the variables in attributes
-/datum/parsed_map/proc/instance_atom(path,list/attributes, turf/crds, no_changeturf, placeOnTop, delete)
- GLOB._preloader.setup(attributes, path)
-
- if(crds)
- if(ispath(path, /turf))
- if(delete)
- for(var/atom/A as anything in crds.GetAllTurfStrictContents())
- if(isobserver(A))
- continue
- qdel(A, force=TRUE)
-
- if(placeOnTop)
- . = crds.PlaceOnTop(null, path, CHANGETURF_DEFER_CHANGE | (no_changeturf ? CHANGETURF_SKIP : NONE))
- else if(!no_changeturf)
- . = crds.ChangeTurf(path, null, CHANGETURF_DEFER_CHANGE)
- else
- . = create_atom(path, crds)//first preloader pass
- else
- . = create_atom(path, crds)//first preloader pass
-
- if(GLOB.use_preloader && .)//second preloader pass, for those atoms that don't ..() in New()
- GLOB._preloader.load(.)
-
- //custom CHECK_TICK here because we don't want things created while we're sleeping to not initialize
- if(TICK_CHECK)
- SSatoms.map_loader_stop()
- stoplag()
- SSatoms.map_loader_begin()
-
/datum/parsed_map/proc/create_atom(path, crds)
set waitfor = FALSE
. = new path (crds)
-//text trimming (both directions) helper proc
-//optionally removes quotes before and after the text (for variable name)
-/datum/parsed_map/proc/trim_text(what as text,trim_quotes=0)
- if(trim_quotes)
- return trimQuotesRegex.Replace(what, "")
- else
- return trimRegex.Replace(what, "")
-
-
//find the position of the next delimiter,skipping whatever is comprised between opening_escape and closing_escape
//returns 0 if reached the last delimiter
/datum/parsed_map/proc/find_next_delimiter_position(text as text,initial_position as num, delimiter=",",opening_escape="\"",closing_escape="\"")
@@ -410,7 +1007,6 @@
return next_delimiter
-
//build a list from variables in text form (e.g {var1="derp"; var2; var3=7} => list(var1="derp", var2, var3=7))
//return the filled list
/datum/parsed_map/proc/readlist(text as text, delimiter=",")
@@ -418,28 +1014,29 @@
if (!text)
return
+ // If we're using a semi colon, we can do this as splittext rather then constant calls to find_next_delimiter_position
+ // This does make the code a bit harder to read, but saves a good bit of time so suck it up
var/position
var/old_position = 1
-
while(position != 0)
// find next delimiter that is not within "..."
position = find_next_delimiter_position(text,old_position,delimiter)
// check if this is a simple variable (as in list(var1, var2)) or an associative one (as in list(var1="foo",var2=7))
var/equal_position = findtext(text,"=",old_position, position)
-
- var/trim_left = trim_text(copytext(text,old_position,(equal_position ? equal_position : position)))
- var/left_constant = delimiter == ";" ? trim_left : parse_constant(trim_left)
+ var/trim_left = TRIM_TEXT(copytext(text,old_position,(equal_position ? equal_position : position)))
+ var/left_constant = parse_constant(trim_left)
if(position)
old_position = position + length(text[position])
+ if(!left_constant) // damn newlines man. Exists to provide behavior consistency with the above loop. not a major cost becuase this path is cold
+ continue
if(equal_position && !isnum(left_constant))
// Associative var, so do the association.
// Note that numbers cannot be keys - the RHS is dropped if so.
- var/trim_right = trim_text(copytext(text, equal_position + length(text[equal_position]), position))
+ var/trim_right = TRIM_TEXT(copytext(text, equal_position + length(text[equal_position]), position))
var/right_constant = parse_constant(trim_right)
.[left_constant] = right_constant
-
else // simple var
. += list(left_constant)
@@ -451,7 +1048,10 @@
// string
if(text[1] == "\"")
- return copytext(text, length(text[1]) + 1, findtext(text, "\"", length(text[1]) + 1))
+ // insert implied locate \" and length("\"") here
+ // It's a minimal timesave but it is a timesave
+ // Safe becuase we're guarenteed trimmed constants
+ return copytext(text, 2, -1)
// list
if(copytext(text, 1, 6) == "list(")//6 == length("list(") + 1
@@ -479,4 +1079,17 @@
/datum/parsed_map/Destroy()
..()
+ SSatoms.map_loader_stop(REF(src)) // Just in case, I don't want to double up here
+ if(turf_blacklist)
+ turf_blacklist.Cut()
+ parsed_bounds.Cut()
+ bounds.Cut()
+ grid_models.Cut()
+ gridSets.Cut()
return QDEL_HINT_HARDDEL_NOW
+
+#undef MAP_DMM
+#undef MAP_TGM
+#undef MAP_UNKNOWN
+#undef TRIM_TEXT
+#undef MAPLOADING_CHECK_TICK
diff --git a/code/modules/mapping/space_management/space_level.dm b/code/modules/mapping/space_management/space_level.dm
index 861258aa20..48303b5e39 100644
--- a/code/modules/mapping/space_management/space_level.dm
+++ b/code/modules/mapping/space_management/space_level.dm
@@ -4,13 +4,20 @@
var/list/traits
var/z_value = 1 //actual z placement
var/linkage = SELFLOOPING
- var/x_bounds
- var/y_bounds
+ /// Bounds at time of loading the map
+ var/bounds
/datum/space_level/New(new_z, new_name, list/new_traits = list())
z_value = new_z
name = new_name
traits = new_traits
+
+ if (islist(new_traits))
+ for (var/trait in new_traits)
+ SSmapping.z_trait_levels[trait] += list(new_z)
+ else // in case a single trait is passed in
+ SSmapping.z_trait_levels[new_traits] += list(new_z)
//set_linkage(new_traits[ZTRAIT_LINKAGE])
- x_bounds = world.maxx
- y_bounds = world.maxy
+
+ //Lazy Init value, will be hopefully changed by SSmapping
+ bounds = list(1, 1, z_value, world.maxx, world.maxy, z_value)
diff --git a/code/modules/mapping/space_management/space_reservation.dm b/code/modules/mapping/space_management/space_reservation.dm
index adaff04000..3c47ac3b5c 100644
--- a/code/modules/mapping/space_management/space_reservation.dm
+++ b/code/modules/mapping/space_management/space_reservation.dm
@@ -1,81 +1,240 @@
+/// Cordon area surrounding turf reservations
+/area/misc/cordon
+ name = "CORDON"
+ icon_state = "cordon"
+ static_lighting = FALSE
+ base_lighting_alpha = 255
+ requires_power = FALSE
+
+#define CORDON_TURF_TYPE /turf/closed/cordon
//Yes, they can only be rectangular.
//Yes, I'm sorry.
/datum/turf_reservation
+ /// All turfs that we've reserved
var/list/reserved_turfs = list()
+
+ /// Turfs around the reservation for cordoning
+ var/list/cordon_turfs = list()
+
+ /// Area of turfs next to the cordon to fill with pre_cordon_area's
+ var/list/pre_cordon_turfs = list()
+
+ /// The width of the reservation
var/width = 0
+
+ /// The height of the reservation
var/height = 0
- var/bottom_left_coords[3]
- var/top_right_coords[3]
- var/wipe_reservation_on_release = TRUE
+
+ /// The z stack size of the reservation. Note that reservations are ALWAYS reserved from the bottom up
+ var/z_size = 0
+
+ /// List of the bottom left turfs. Indexed by what their z index for this reservation is
+ var/list/bottom_left_turfs = list()
+
+ /// List of the top right turfs. Indexed by what their z index for this reservation is
+ var/list/top_right_turfs = list()
+
+ /// The turf type the reservation is initially made with
var/turf_type = /turf/open/space
+ ///Distance away from the cordon where we can put a "sort-cordon" and run some extra code (see make_repel). 0 makes nothing happen
+ var/pre_cordon_distance = 0
+
/datum/turf_reservation/transit
turf_type = /turf/open/space/transit
+ pre_cordon_distance = 7
/datum/turf_reservation/interior
turf_type = /turf/open/void/vehicle
/datum/turf_reservation/proc/Release()
- var/v = reserved_turfs.Copy()
- for(var/i in reserved_turfs)
- var/turf/T = i
- T.flags_atom |= UNUSED_RESERVATION_TURF
- reserved_turfs -= i
- SSmapping.used_turfs -= i
- SSmapping.reserve_turfs(v)
+ bottom_left_turfs.Cut()
+ top_right_turfs.Cut()
+
+ var/list/reserved_copy = reserved_turfs.Copy()
+ SSmapping.used_turfs -= reserved_turfs
+ reserved_turfs = list()
+
+ var/list/cordon_copy = cordon_turfs.Copy()
+ SSmapping.used_turfs -= cordon_turfs
+ cordon_turfs = list()
-/datum/turf_reservation/proc/Reserve(width, height, zlevel)
+ var/release_turfs = reserved_copy + cordon_copy
+
+ for(var/turf/reserved_turf as anything in release_turfs)
+ SEND_SIGNAL(reserved_turf, COMSIG_TURF_RESERVATION_RELEASED, src)
+
+ // Makes the linter happy, even tho we don't await this
+ INVOKE_ASYNC(SSmapping, TYPE_PROC_REF(/datum/controller/subsystem/mapping, reserve_turfs), release_turfs)
+
+/// Attempts to calaculate and store a list of turfs around the reservation for cordoning. Returns whether a valid cordon was calculated
+/datum/turf_reservation/proc/calculate_cordon_turfs(turf/bottom_left, turf/top_right)
+ if(bottom_left.x < 2 || bottom_left.y < 2 || top_right.x > (world.maxx - 2) || top_right.y > (world.maxy - 2))
+ return FALSE // no space for a cordon here
+
+ var/list/possible_turfs = CORNER_OUTLINE(bottom_left, width, height)
+ // if they're our cordon turfs, accept them
+ possible_turfs -= cordon_turfs
+ for(var/turf/cordon_turf as anything in possible_turfs)
+ if(!(cordon_turf.turf_flags & UNUSED_RESERVATION_TURF))
+ return FALSE
+ cordon_turfs |= possible_turfs
+
+ if(pre_cordon_distance)
+ var/turf/offset_turf = locate(bottom_left.x + pre_cordon_distance, bottom_left.y + pre_cordon_distance, bottom_left.z)
+ var/list/to_add = CORNER_OUTLINE(offset_turf, width - pre_cordon_distance * 2, height - pre_cordon_distance * 2) //we step-by-stop move inwards from the outer cordon
+ for(var/turf/turf_being_added as anything in to_add)
+ pre_cordon_turfs |= turf_being_added //add one by one so we can filter out duplicates
+
+ return TRUE
+
+/// Actually generates the cordon around the reservation, and marking the cordon turfs as reserved
+/datum/turf_reservation/proc/generate_cordon()
+ for(var/turf/cordon_turf as anything in cordon_turfs)
+ var/area/misc/cordon/cordon_area = GLOB.areas_by_type[/area/misc/cordon] || new
+ //var/area/old_area = cordon_turf.loc
+ //old_area.turfs_to_uncontain += cordon_turf
+ //cordon_area.contained_turfs += cordon_turf
+ cordon_area.contents += cordon_turf
+ // Its no longer unused, but its also not "used"
+ cordon_turf.turf_flags &= ~UNUSED_RESERVATION_TURF
+ cordon_turf.ChangeTurf(CORDON_TURF_TYPE, CORDON_TURF_TYPE)
+ SSmapping.unused_turfs["[cordon_turf.z]"] -= cordon_turf
+ // still gets linked to us though
+ SSmapping.used_turfs[cordon_turf] = src
+
+ //swap the area with the pre-cordoning area
+
+/// Internal proc which handles reserving the area for the reservation.
+/datum/turf_reservation/proc/_reserve_area(width, height, zlevel)
+ src.width = width
+ src.height = height
if(width > world.maxx || height > world.maxy || width < 1 || height < 1)
- log_debug("turf reservation had invalid dimensions")
return FALSE
var/list/avail = SSmapping.unused_turfs["[zlevel]"]
- var/turf/bottom_left
- var/turf/top_right
+ var/turf/BL
+ var/turf/TR
var/list/turf/final = list()
var/passing = FALSE
for(var/i in avail)
CHECK_TICK
- bottom_left = i
- if(!(bottom_left.flags_atom & UNUSED_RESERVATION_TURF))
+ BL = i
+ if(!(BL.turf_flags & UNUSED_RESERVATION_TURF))
continue
- if(bottom_left.x + width > world.maxx || bottom_left.y + height > world.maxy)
+ if(BL.x + width > world.maxx || BL.y + height > world.maxy)
continue
- top_right = locate(bottom_left.x + width - 1, bottom_left.y + height - 1, bottom_left.z)
- if(!(top_right.flags_atom & UNUSED_RESERVATION_TURF))
+ TR = locate(BL.x + width - 1, BL.y + height - 1, BL.z)
+ if(!(TR.turf_flags & UNUSED_RESERVATION_TURF))
continue
- final = block(bottom_left, top_right)
+ final = block(BL, TR)
if(!final)
continue
passing = TRUE
- for(var/turf/checking as anything in final)
- if(!(checking.flags_atom & UNUSED_RESERVATION_TURF))
+ for(var/I in final)
+ var/turf/checking = I
+ if(!(checking.turf_flags & UNUSED_RESERVATION_TURF))
passing = FALSE
break
+ if(passing) // found a potentially valid area, now try to calculate its cordon
+ passing = calculate_cordon_turfs(BL, TR)
if(!passing)
continue
break
- if(!passing || !istype(bottom_left) || !istype(top_right))
- log_debug("failed to pass reservation tests, [passing], [istype(bottom_left)], [istype(top_right)]")
+ if(!passing || !istype(BL) || !istype(TR))
return FALSE
- bottom_left_coords = list(bottom_left.x, bottom_left.y, bottom_left.z)
- top_right_coords = list(top_right.x, top_right.y, top_right.z)
- var/weakref = WEAKREF(src)
for(var/i in final)
var/turf/T = i
reserved_turfs |= T
SSmapping.unused_turfs["[T.z]"] -= T
- SSmapping.used_turfs[T] = weakref
- T = T.ChangeTurf(turf_type, turf_type)
- T.flags_atom &= ~UNUSED_RESERVATION_TURF
- src.width = width
- src.height = height
+ SSmapping.used_turfs[T] = src
+ T.turf_flags = (T.turf_flags | RESERVATION_TURF) & ~UNUSED_RESERVATION_TURF
+ T.ChangeTurf(turf_type, turf_type)
+
+ bottom_left_turfs += BL
+ top_right_turfs += TR
+ return TRUE
+
+/datum/turf_reservation/proc/reserve(width, height, z_size, z_reservation)
+ src.z_size = z_size
+ var/failed_reservation = FALSE
+ for(var/_ in 1 to z_size)
+ if(!_reserve_area(width, height, z_reservation))
+ failed_reservation = TRUE
+ break
+
+ if(failed_reservation)
+ Release()
+ return FALSE
+
+ generate_cordon()
return TRUE
+/// Calculates the effective bounds information for the given turf. Returns a list of the information, or null if not applicable.
+/datum/turf_reservation/proc/calculate_turf_bounds_information(turf/target)
+ for(var/z_idx in 1 to z_size)
+ var/turf/bottom_left = bottom_left_turfs[z_idx]
+ var/turf/top_right = top_right_turfs[z_idx]
+ var/bl_x = bottom_left.x
+ var/bl_y = bottom_left.y
+ var/tr_x = top_right.x
+ var/tr_y = top_right.y
+
+ if(target.x < bl_x)
+ continue
+
+ if(target.y < bl_y)
+ continue
+
+ if(target.x > tr_x)
+ continue
+
+ if(target.y > tr_y)
+ continue
+
+ var/list/return_information = list()
+ return_information["z_idx"] = z_idx
+ return_information["offset_x"] = target.x - bl_x
+ return_information["offset_y"] = target.y - bl_y
+ return return_information
+ return null
+
+/// Gets the turf below the given target. Returns null if there is no turf below the target
+/datum/turf_reservation/proc/get_turf_below(turf/target)
+ var/list/bounds_info = calculate_turf_bounds_information(target)
+ if(isnull(bounds_info))
+ return null
+
+ var/z_idx = bounds_info["z_idx"]
+ // check what z level, if its the max, then there is no turf below
+ if(z_idx == z_size)
+ return null
+
+ var/offset_x = bounds_info["offset_x"]
+ var/offset_y = bounds_info["offset_y"]
+ var/turf/bottom_left = bottom_left_turfs[z_idx + 1]
+ return locate(bottom_left.x + offset_x, bottom_left.y + offset_y, bottom_left.z)
+
+/// Gets the turf above the given target. Returns null if there is no turf above the target
+/datum/turf_reservation/proc/get_turf_above(turf/target)
+ var/list/bounds_info = calculate_turf_bounds_information(target)
+ if(isnull(bounds_info))
+ return null
+
+ var/z_idx = bounds_info["z_idx"]
+ // check what z level, if its the min, then there is no turf above
+ if(z_idx == 1)
+ return null
+
+ var/offset_x = bounds_info["offset_x"]
+ var/offset_y = bounds_info["offset_y"]
+ var/turf/bottom_left = bottom_left_turfs[z_idx - 1]
+ return locate(bottom_left.x + offset_x, bottom_left.y + offset_y, bottom_left.z)
+
/datum/turf_reservation/New()
LAZYADD(SSmapping.turf_reservations, src)
/datum/turf_reservation/Destroy()
- INVOKE_ASYNC(src, PROC_REF(Release))
+ Release()
LAZYREMOVE(SSmapping.turf_reservations, src)
return ..()
diff --git a/code/modules/mapping/space_management/zlevel_manager.dm b/code/modules/mapping/space_management/zlevel_manager.dm
index 9311719ea7..2b96065929 100644
--- a/code/modules/mapping/space_management/zlevel_manager.dm
+++ b/code/modules/mapping/space_management/zlevel_manager.dm
@@ -14,16 +14,22 @@
for (var/I in 1 to default_map_traits.len)
var/list/features = default_map_traits[I]
var/datum/space_level/S = new(I, features[DL_NAME], features[DL_TRAITS])
- z_list += S
+ manage_z_level(S, filled_with_space = FALSE)
+ //generate_z_level_linkages() // Default Zs don't use add_new_zlevel() so they don't automatically generate z-linkages.
-/datum/controller/subsystem/mapping/proc/add_new_zlevel(name, traits = list(), z_type = /datum/space_level)
+/datum/controller/subsystem/mapping/proc/add_new_zlevel(name, traits = list(), z_type = /datum/space_level, contain_turfs = TRUE)
+ UNTIL(!adding_new_zlevel)
+ adding_new_zlevel = TRUE
var/new_z = z_list.len + 1
if (world.maxz < new_z)
world.incrementMaxZ()
CHECK_TICK
// TODO: sleep here if the Z level needs to be cleared
var/datum/space_level/S = new z_type(new_z, name, traits)
- z_list += S
+ manage_z_level(S, filled_with_space = TRUE, contain_turfs = contain_turfs)
+ //generate_linkages_for_z_level(new_z)
+ //calculate_z_level_gravity(new_z)
+ adding_new_zlevel = FALSE
SEND_GLOBAL_SIGNAL(COMSIG_GLOB_NEW_Z, S)
return S
diff --git a/code/modules/mob/living/carbon/human/human.dm b/code/modules/mob/living/carbon/human/human.dm
index 837f193c4c..fd979fb6a4 100644
--- a/code/modules/mob/living/carbon/human/human.dm
+++ b/code/modules/mob/living/carbon/human/human.dm
@@ -780,25 +780,7 @@
R.fields[text("com_[counter]")] = text("Made by [U.name] ([U.modtype] [U.braintype]) on [time2text(world.realtime, "DDD MMM DD hh:mm:ss")], [game_year]
[t1]")
if(href_list["medholocard"])
- if(!skillcheck(usr, SKILL_MEDICAL, SKILL_MEDICAL_MEDIC))
- to_chat(usr, SPAN_WARNING("You're not trained to use this."))
- return
- if(!has_species(src, "Human"))
- to_chat(usr, SPAN_WARNING("Triage holocards only works on humans."))
- return
- var/newcolor = tgui_input_list(usr, "Choose a triage holo card to add to the patient:", "Triage holo card", list("black", "red", "orange", "none"))
- if(!newcolor) return
- if(get_dist(usr, src) > 7)
- to_chat(usr, SPAN_WARNING("[src] is too far away."))
- return
- if(newcolor == "none")
- if(!holo_card_color) return
- holo_card_color = null
- to_chat(usr, SPAN_NOTICE("You remove the holo card on [src]."))
- else if(newcolor != holo_card_color)
- holo_card_color = newcolor
- to_chat(usr, SPAN_NOTICE("You add a [newcolor] holo card on [src]."))
- update_targeted()
+ change_holo_card(usr)
if(href_list["lookitem"])
var/obj/item/I = locate(href_list["lookitem"])
@@ -850,6 +832,39 @@
..()
return
+/mob/living/carbon/human/proc/change_holo_card(mob/user)
+ if(isobserver(user))
+ return
+ if(!skillcheck(user, SKILL_MEDICAL, SKILL_MEDICAL_MEDIC))
+ // Removing your own holocard when you are not trained
+ if(user == src && holo_card_color)
+ if(tgui_alert(user, "Are you sure you want to reset your own holocard?", "Resetting Holocard", list("Yes", "No")) != "Yes")
+ return
+ holo_card_color = null
+ to_chat(user, SPAN_NOTICE("You reset your holocard."))
+ hud_set_holocard()
+ return
+ to_chat(user, SPAN_WARNING("You're not trained to use this."))
+ return
+ if(!has_species(src, "Human"))
+ to_chat(user, SPAN_WARNING("Triage holocards only works on humans."))
+ return
+ var/newcolor = tgui_input_list(user, "Choose a triage holo card to add to the patient:", "Triage holo card", list("black", "red", "orange", "purple", "none"))
+ if(!newcolor)
+ return
+ if(get_dist(user, src) > 7)
+ to_chat(user, SPAN_WARNING("[src] is too far away."))
+ return
+ if(newcolor == "none")
+ if(!holo_card_color)
+ return
+ holo_card_color = null
+ to_chat(user, SPAN_NOTICE("You remove the holo card on [src]."))
+ else if(newcolor != holo_card_color)
+ holo_card_color = newcolor
+ to_chat(user, SPAN_NOTICE("You add a [newcolor] holo card on [src]."))
+ hud_set_holocard()
+
/mob/living/carbon/human/tgui_interact(mob/user, datum/tgui/ui) // I'M SORRY, SO FUCKING SORRY
. = ..()
ui = SStgui.try_update_ui(user, src, ui)
diff --git a/code/modules/mob/living/carbon/human/human_attackhand.dm b/code/modules/mob/living/carbon/human/human_attackhand.dm
index 2bb113d677..d9dc83579c 100644
--- a/code/modules/mob/living/carbon/human/human_attackhand.dm
+++ b/code/modules/mob/living/carbon/human/human_attackhand.dm
@@ -187,12 +187,6 @@
/mob/living/carbon/human/help_shake_act(mob/living/carbon/M)
//Target is us
if(src == M)
- if(holo_card_color) //if we have a triage holocard printed on us, we remove it.
- holo_card_color = null
- update_targeted()
- visible_message(SPAN_NOTICE("[src] removes the holo card on [gender==MALE?"himself":"herself"]."), \
- SPAN_NOTICE("You remove the holo card on yourself."), null, 3)
- return
check_for_injuries()
return
diff --git a/code/modules/mob/living/carbon/human/human_damage.dm b/code/modules/mob/living/carbon/human/human_damage.dm
index 5c685cc3ac..e5da9cd414 100644
--- a/code/modules/mob/living/carbon/human/human_damage.dm
+++ b/code/modules/mob/living/carbon/human/human_damage.dm
@@ -312,18 +312,18 @@ In most cases it makes more sense to use apply_damage() instead! And make sure t
if(update) UpdateDamageIcon()
// damage MANY limbs, in random order
-/mob/living/carbon/human/take_overall_damage(brute, burn, sharp = 0, edge = 0, used_weapon = null)
+/mob/living/carbon/human/take_overall_damage(brute, burn, used_weapon = null, limb_damage_chance = 80)
if(status_flags & GODMODE)
return //godmode
- var/list/obj/limb/parts = get_damageable_limbs(80)
+ var/list/obj/limb/parts = get_damageable_limbs(limb_damage_chance)
var/amount_of_parts = length(parts)
for(var/obj/limb/L as anything in parts)
- L.take_damage(brute / amount_of_parts, burn / amount_of_parts, sharp, edge, used_weapon)
+ L.take_damage(brute / amount_of_parts, burn / amount_of_parts, sharp = FALSE, edge = FALSE, used_weapon = used_weapon)
updatehealth()
UpdateDamageIcon()
-// damage MANY LIMBS, in random order
-/mob/living/carbon/human/proc/take_overall_armored_damage(damage, armour_type = ARMOR_MELEE, damage_type = BRUTE, limb_damage_chance = 80, penetration = 0, armour_break_pr_pen = 0, armour_break_flat = 0)
+// damage MANY LIMBS, in random order, but consider armor
+/mob/living/carbon/human/proc/take_overall_armored_damage(damage, armour_type = ARMOR_MELEE, damage_type = BRUTE, limb_damage_chance = 80, penetration = 0)
if(status_flags & GODMODE)
return //godmode
var/list/obj/limb/parts = get_damageable_limbs(limb_damage_chance)
@@ -331,6 +331,8 @@ In most cases it makes more sense to use apply_damage() instead! And make sure t
var/armour_config = GLOB.marine_ranged
if(armour_type == ARMOR_MELEE)
armour_config = GLOB.marine_melee
+ if(armour_type == ARMOR_BOMB)
+ armour_config = GLOB.marine_explosive
for(var/obj/limb/L as anything in parts)
var/armor = getarmor(L, armour_type)
var/modified_damage = armor_damage_reduction(armour_config, damage, armor, penetration, 0, 0)
diff --git a/code/modules/mob/living/carbon/human/human_defines.dm b/code/modules/mob/living/carbon/human/human_defines.dm
index a3424a1815..5772c8140c 100644
--- a/code/modules/mob/living/carbon/human/human_defines.dm
+++ b/code/modules/mob/living/carbon/human/human_defines.dm
@@ -139,7 +139,7 @@
var/last_chew = 0
//taken from human.dm
- hud_possible = list(HEALTH_HUD,STATUS_HUD, STATUS_HUD_OOC, STATUS_HUD_XENO_INFECTION, STATUS_HUD_XENO_CULTIST, ID_HUD, WANTED_HUD, ORDER_HUD, XENO_HOSTILE_ACID, XENO_HOSTILE_SLOW, XENO_HOSTILE_TAG, XENO_HOSTILE_FREEZE, HUNTER_CLAN, HUNTER_HUD, FACTION_HUD)
+ hud_possible = list(HEALTH_HUD, STATUS_HUD, STATUS_HUD_OOC, STATUS_HUD_XENO_INFECTION, STATUS_HUD_XENO_CULTIST, ID_HUD, WANTED_HUD, ORDER_HUD, XENO_HOSTILE_ACID, XENO_HOSTILE_SLOW, XENO_HOSTILE_TAG, XENO_HOSTILE_FREEZE, HUNTER_CLAN, HUNTER_HUD, FACTION_HUD, HOLOCARD_HUD)
var/embedded_flag //To check if we've need to roll for damage on movement while an item is imbedded in us.
var/allow_gun_usage = TRUE
var/melee_allowed = TRUE
diff --git a/code/modules/mob/living/carbon/human/update_icons.dm b/code/modules/mob/living/carbon/human/update_icons.dm
index 2952aa7197..990f53e639 100644
--- a/code/modules/mob/living/carbon/human/update_icons.dm
+++ b/code/modules/mob/living/carbon/human/update_icons.dm
@@ -225,22 +225,6 @@ There are several things that need to be remembered:
overlays_standing[HAIR_LAYER] = hair_s
apply_overlay(HAIR_LAYER)
-//Call when target overlay should be added/removed
-/mob/living/carbon/human/update_targeted()
- remove_overlay(TARGETED_LAYER)
-
- var/image/holo_card_image
-
- if(holo_card_color)
- holo_card_image = image("icon" = 'icons/effects/Targeted.dmi', "icon_state" = "holo_card_[holo_card_color]")
-
- if(!holo_card_image)
- return
-
- holo_card_image.layer = -TARGETED_LAYER
- overlays_standing[TARGETED_LAYER] = holo_card_image
- apply_overlay(TARGETED_LAYER)
-
//Call when someone is gauzed or splinted, or when one of those items are removed
/mob/living/carbon/human/update_med_icon()
remove_overlay(MEDICAL_LAYER)
diff --git a/code/modules/mob/living/living_health_procs.dm b/code/modules/mob/living/living_health_procs.dm
index 7801549528..73f02f9b3e 100644
--- a/code/modules/mob/living/living_health_procs.dm
+++ b/code/modules/mob/living/living_health_procs.dm
@@ -401,7 +401,7 @@
src.updatehealth()
// damage MANY limbs, in random order
-/mob/living/proc/take_overall_damage(brute, burn, used_weapon = null)
+/mob/living/proc/take_overall_damage(brute, burn, used_weapon = null, limb_damage_chance = 80)
if(status_flags & GODMODE) return 0 //godmode
apply_damage(brute, BRUTE)
apply_damage(burn, BURN)
diff --git a/code/modules/mob/living/living_healthscan.dm b/code/modules/mob/living/living_healthscan.dm
index f3355157a4..d21f31fe00 100644
--- a/code/modules/mob/living/living_healthscan.dm
+++ b/code/modules/mob/living/living_healthscan.dm
@@ -79,6 +79,12 @@ GLOBAL_LIST_INIT(known_implants, subtypesof(/obj/item/implant))
// Fake death will make the scanner think they died of oxygen damage, thus it returns enough damage to kill minus already recieved damage.
return round(POSITIVE(200 - total_mob_damage))
+/datum/health_scan/proc/get_holo_card_color(mob/living/target_mob)
+ if(!ishuman(target_mob))
+ return
+ var/mob/living/carbon/human/human_mob = target_mob
+ return human_mob.holo_card_color
+
/datum/health_scan/proc/get_health_value(mob/living/target_mob)
if(!(target_mob.status_flags & FAKEDEATH))
return target_mob.health
@@ -97,7 +103,7 @@ GLOBAL_LIST_INIT(known_implants, subtypesof(/obj/item/implant))
"clone" = round(target_mob.getCloneLoss()),
"blood_type" = target_mob.blood_type,
"blood_amount" = target_mob.blood_volume,
-
+ "holocard" = get_holo_card_color(target_mob),
"hugged" = (locate(/obj/item/alien_embryo) in target_mob),
)
@@ -450,6 +456,17 @@ GLOBAL_LIST_INIT(known_implants, subtypesof(/obj/item/implant))
return data
+/datum/health_scan/ui_act(action, list/params, datum/tgui/ui, datum/ui_state/state)
+ . = ..()
+ if(.)
+ return
+ switch(action)
+ if("change_holo_card")
+ if(ishuman(target_mob))
+ var/mob/living/carbon/human/target_human = target_mob
+ target_human.change_holo_card(ui.user)
+ return TRUE
+
/// legacy proc for to_chat messages on health analysers
/mob/living/proc/health_scan(mob/living/carbon/human/user, ignore_delay = FALSE, show_limb_damage = TRUE, show_browser = TRUE, alien = FALSE, do_checks = TRUE) // ahem. FUCK WHOEVER CODED THIS SHIT AS NUMBERS AND NOT DEFINES.
if(do_checks)
diff --git a/code/modules/mob/living/silicon/robot/drone/drone_damage.dm b/code/modules/mob/living/silicon/robot/drone/drone_damage.dm
index a0245d8c8c..b27ebdd3c4 100644
--- a/code/modules/mob/living/silicon/robot/drone/drone_damage.dm
+++ b/code/modules/mob/living/silicon/robot/drone/drone_damage.dm
@@ -1,5 +1,5 @@
//Redefining some robot procs, since drones can't be repaired and really shouldn't take component damage.
-/mob/living/silicon/robot/drone/take_overall_damage(brute = 0, burn = 0, sharp = 0, used_weapon = null)
+/mob/living/silicon/robot/drone/take_overall_damage(brute = 0, burn = 0, sharp = 0, used_weapon = null, limb_damage_chance = 80)
bruteloss += brute
fireloss += burn
diff --git a/code/modules/mob/living/silicon/robot/robot_damage.dm b/code/modules/mob/living/silicon/robot/robot_damage.dm
index 78b6b78445..fc40944505 100644
--- a/code/modules/mob/living/silicon/robot/robot_damage.dm
+++ b/code/modules/mob/living/silicon/robot/robot_damage.dm
@@ -108,7 +108,7 @@
parts -= picked
-/mob/living/silicon/robot/take_overall_damage(brute = 0, burn = 0, sharp = 0, used_weapon = null)
+/mob/living/silicon/robot/take_overall_damage(brute = 0, burn = 0, sharp = 0, used_weapon = null, limb_damage_chance = 80)
if(status_flags & GODMODE) return //godmode
var/list/datum/robot_component/parts = get_damageable_components()
diff --git a/code/modules/mob/mob.dm b/code/modules/mob/mob.dm
index 3df4c8b83f..4c765d6223 100644
--- a/code/modules/mob/mob.dm
+++ b/code/modules/mob/mob.dm
@@ -102,6 +102,8 @@
I = image('icons/mob/hud/sec_hud.dmi', src, "")
if(HUNTER_CLAN,HUNTER_HUD)
I = image('icons/mob/hud/hud_yautja.dmi', src, "")
+ if(HOLOCARD_HUD)
+ I = image('icons/mob/hud/marine_hud.dmi', src, "")
I.appearance_flags |= NO_CLIENT_COLOR|KEEP_APART|RESET_COLOR
hud_list[hud] = I
diff --git a/code/modules/nightmare/nmtasks/mapload.dm b/code/modules/nightmare/nmtasks/mapload.dm
index 1ace7cd2fb..d53b566319 100644
--- a/code/modules/nightmare/nmtasks/mapload.dm
+++ b/code/modules/nightmare/nmtasks/mapload.dm
@@ -53,7 +53,7 @@
if(!target_turf?.z)
log_debug("Nightmare Mapload: Could not find landmark: [landmark]")
return
- var/result = parsed.load(target_turf.x, target_turf.y, target_turf.z, cropMap = TRUE, no_changeturf = FALSE, placeOnTop = FALSE, delete = replace)
+ var/result = parsed.load(target_turf.x, target_turf.y, target_turf.z, crop_map = TRUE, no_changeturf = FALSE, place_on_top = FALSE, delete = replace)
if(!result || !parsed.bounds)
log_debug("Nightmare Mapload: Map insertion failed unexpectedly for file: [filepath]")
return
@@ -66,22 +66,23 @@
log_debug("Nightmare Mapload: Loaded map file but could not initialize: '[filepath]' at ([target_turf.x], [target_turf.y], [target_turf.z])")
return TRUE
-/// Initialize atoms/areas in bounds
+/// Initialize atoms/areas in bounds - basically a Nightmare version of [/datum/map_template/initTemplateBounds]
/datum/nmtask/mapload/proc/initialize_boundary_contents()
var/list/bounds = parsed.bounds
if(length(bounds) < 6)
return
- var/list/TT = block( locate(bounds[MAP_MINX], bounds[MAP_MINY], bounds[MAP_MINZ]),
+ var/list/turf_block = block( locate(bounds[MAP_MINX], bounds[MAP_MINY], bounds[MAP_MINZ]),
locate(bounds[MAP_MAXX], bounds[MAP_MAXY], bounds[MAP_MAXZ]))
var/list/area/arealist = list()
var/list/atom/atomlist = list()
- for(var/turf/T as anything in TT)
- atomlist |= T
- if(T.loc) arealist |= T.loc
- for(var/A in T)
- atomlist |= A
- SSmapping.reg_in_areas_in_z(arealist)
- SSatoms.InitializeAtoms(atomlist)
- // We still defer lighting, area sorting, etc
+ for(var/turf/turf as anything in turf_block)
+ atomlist += turf
+ if(turf.loc)
+ arealist |= turf.loc
+ for(var/atom/movable/movable as anything in turf)
+ atomlist += movable // Much like initTemplateBounds() this only recurses content once. Never been an issue so far, but keep it in mind.
+ SSmapping.reg_in_areas_in_z(arealist) // Legacy. Not sure this is needed as it should already be carried out through area Initialize.
+ SSatoms.InitializeAtoms(atomlist + arealist)
+ // We still defer lighting, area sorting, etc, to be done all in one go!
SEND_SIGNAL(src, COMSIG_NIGHTMARE_TAINTED_BOUNDS, bounds)
return TRUE
diff --git a/code/modules/power/apc.dm b/code/modules/power/apc.dm
index b210dbac29..e0d0998c82 100644
--- a/code/modules/power/apc.dm
+++ b/code/modules/power/apc.dm
@@ -210,6 +210,7 @@ GLOBAL_LIST_INIT(apc_wire_descriptions, list(
"chargingStatus" = charging,
"totalLoad" = display_power(lastused_total),
"coverLocked" = coverlocked,
+ "siliconUser" = FALSE,
"powerChannels" = list(
list(
diff --git a/code/modules/shuttle/computer.dm b/code/modules/shuttle/computer.dm
index 3d0c8fca14..549ddb3bfa 100644
--- a/code/modules/shuttle/computer.dm
+++ b/code/modules/shuttle/computer.dm
@@ -81,8 +81,8 @@
to_chat(usr, SPAN_NOTICE("Unable to comply."))
return TRUE
-/obj/structure/machinery/computer/shuttle/connect_to_shuttle(obj/docking_port/mobile/port, obj/docking_port/stationary/dock, idnum, override=FALSE)
- if(port && (shuttleId == initial(shuttleId) || override))
+/obj/structure/machinery/computer/shuttle/connect_to_shuttle(mapload, obj/docking_port/mobile/port, obj/docking_port/stationary/dock)
+ if(port && (shuttleId == initial(shuttleId)))
shuttleId = port.id
/obj/structure/machinery/computer/shuttle/ert
diff --git a/code/modules/shuttle/computers/dropship_computer.dm b/code/modules/shuttle/computers/dropship_computer.dm
index 3f688c5721..931c4a799c 100644
--- a/code/modules/shuttle/computers/dropship_computer.dm
+++ b/code/modules/shuttle/computers/dropship_computer.dm
@@ -379,7 +379,7 @@
update_equipment(is_optimised, FALSE)
var/list/local_data = ui_data(user)
var/found = FALSE
- playsound(loc, get_sfx("terminal_button"), KEYBOARD_SOUND_VOLUME, 1)
+ playsound(loc, get_sfx("terminal_button"), 5, 1)
for(var/destination in local_data["destinations"])
if(destination["id"] == dock_id)
found = TRUE
diff --git a/code/modules/shuttle/docking.dm b/code/modules/shuttle/docking.dm
index a21ec330d4..63e220dead 100644
--- a/code/modules/shuttle/docking.dm
+++ b/code/modules/shuttle/docking.dm
@@ -1,7 +1,5 @@
/// This is the main proc. It instantly moves our mobile port to stationary port `new_dock`.
/obj/docking_port/mobile/proc/initiate_docking(obj/docking_port/stationary/new_dock, movement_direction, force=FALSE)
- // Crashing this ship with NO SURVIVORS
-
if(new_dock.get_docked() == src)
remove_ripples()
return DOCKING_SUCCESS
diff --git a/code/modules/shuttle/shuttle.dm b/code/modules/shuttle/shuttle.dm
index 85fc38bf5f..ad00a0b68f 100644
--- a/code/modules/shuttle/shuttle.dm
+++ b/code/modules/shuttle/shuttle.dm
@@ -19,6 +19,8 @@
* port can be used in these places, or the docking port is compatible, etc.
*/
var/id
+ ///The original template shuttle_id for this shuttle (so without a suffix identifier)
+ var/template_id
///Possible destinations
var/port_destinations
///this should point -away- from the dockingport door, ie towards the ship
@@ -380,10 +382,82 @@
var/shuttle_flags = NONE
+#define WORLDMAXX_CUTOFF (world.maxx + 1)
+#define WORLDMAXY_CUTOFF (world.maxx + 1)
+/**
+ * Calculated and populates the information used for docking and some internal vars.
+ * This can also be used to calculate from shuttle_areas so that you can expand/shrink shuttles!
+ *
+ * Arguments:
+ * * loading_from - The template that the shuttle was loaded from, if not given we iterate shuttle_areas to calculate information instead
+ */
+/obj/docking_port/mobile/proc/calculate_docking_port_information(datum/map_template/shuttle/loading_from)
+ var/port_x_offset = loading_from?.port_x_offset
+ var/port_y_offset = loading_from?.port_y_offset
+ var/width = loading_from?.width
+ var/height = loading_from?.height
+ if(!loading_from)
+ if(!length(shuttle_areas))
+ CRASH("Attempted to calculate a docking port's information without a template before it was assigned any areas!")
+ // no template given, use shuttle_areas to calculate width and height
+ var/min_x = -1
+ var/min_y = -1
+ var/max_x = WORLDMAXX_CUTOFF
+ var/max_y = WORLDMAXY_CUTOFF
+ for(var/area/area as anything in shuttle_areas)
+ for(var/turf/turf in area)
+ min_x = max(turf.x, min_x)
+ max_x = min(turf.x, max_x)
+ min_y = max(turf.y, min_y)
+ max_y = min(turf.y, max_y)
+ CHECK_TICK
+
+ if(min_x == -1 || max_x == WORLDMAXX_CUTOFF)
+ CRASH("Failed to locate shuttle boundaries when iterating through shuttle areas, somehow.")
+ if(min_y == -1 || max_y == WORLDMAXY_CUTOFF)
+ CRASH("Failed to locate shuttle boundaries when iterating through shuttle areas, somehow.")
+
+ width = (max_x - min_x) + 1
+ height = (max_y - min_y) + 1
+ port_x_offset = min_x - x
+ port_y_offset = min_y - y
+
+ if(dir in list(EAST, WEST))
+ src.width = height
+ src.height = width
+ else
+ src.width = width
+ src.height = height
+
+ switch(dir)
+ if(NORTH)
+ dwidth = port_x_offset - 1
+ dheight = port_y_offset - 1
+ if(EAST)
+ dwidth = height - port_y_offset
+ dheight = port_x_offset - 1
+ if(SOUTH)
+ dwidth = width - port_x_offset
+ dheight = height - port_y_offset
+ if(WEST)
+ dwidth = port_y_offset - 1
+ dheight = width - port_x_offset
+#undef WORLDMAXX_CUTOFF
+#undef WORLDMAXY_CUTOFF
+
/obj/docking_port/mobile/register()
. = ..()
SSshuttle.mobile += src
+/**
+ * Actions to be taken after shuttle is loaded and has been moved to its final location
+ *
+ * Arguments:
+ * * replace - TRUE if this shuttle is replacing an existing one. FALSE by default.
+ */
+/obj/docking_port/mobile/proc/postregister(replace = FALSE)
+ return
+
/obj/docking_port/mobile/Destroy(force)
if(force)
QDEL_NULL(alarm_sound_loop)
@@ -442,6 +516,10 @@
// Called after the shuttle is loaded from template
/obj/docking_port/mobile/proc/linkup(datum/map_template/shuttle/template, obj/docking_port/stationary/dock)
+
+ // ================== CM Change ==================
+ // This is gone in /tg/ backend but kept for historical reasons
+ // Suspect this is supposed to be handled in register
var/list/static/shuttle_id = list()
var/idnum = ++shuttle_id[id]
if(idnum > 1)
@@ -449,12 +527,13 @@
id = "[id][idnum]"
if(name == initial(name))
name = "[name] [idnum]"
- for(var/place in shuttle_areas)
- var/area/area = place
- area.connect_to_shuttle(src, dock, idnum, FALSE)
- for(var/each in place)
- var/atom/atom = each
- atom.connect_to_shuttle(src, dock, idnum, FALSE)
+ template_id = template.shuttle_id // Value without the idnum
+ // ================ END CM Change ================
+
+ for(var/area/place as anything in shuttle_areas)
+ place.connect_to_shuttle(TRUE, src, dock)
+ for(var/atom/individual_atoms in place)
+ individual_atoms.connect_to_shuttle(TRUE, src, dock)
//this is a hook for custom behaviour. Maybe at some point we could add checks to see if engines are intact
@@ -1004,3 +1083,15 @@
to_chat(user, SPAN_WARNING("Shuttle already in transit."))
return FALSE
return TRUE
+
+/obj/docking_port/mobile/proc/get_transit_path_type()
+ . = /turf/open/space/transit
+ switch(preferred_direction)
+ if(NORTH)
+ return /turf/open/space/transit/north
+ if(SOUTH)
+ return /turf/open/space/transit/south
+ if(EAST)
+ return /turf/open/space/transit/east
+ if(WEST)
+ return /turf/open/space/transit/west
diff --git a/code/modules/shuttle/shuttles/dropship.dm b/code/modules/shuttle/shuttles/dropship.dm
index bd9beefb61..1f2822b353 100644
--- a/code/modules/shuttle/shuttles/dropship.dm
+++ b/code/modules/shuttle/shuttles/dropship.dm
@@ -29,6 +29,8 @@
var/automated_lz_id
var/automated_delay
var/automated_timer
+ var/datum/cas_signal/paradrop_signal
+
/obj/docking_port/mobile/marine_dropship/Initialize(mapload)
. = ..()
@@ -42,19 +44,29 @@
door_control.add_door(air, "port")
if("aft_door")
door_control.add_door(air, "aft")
- var/obj/structure/machinery/door/airlock/multi_tile/almayer/dropshiprear/hatch = air
- if(istype(hatch))
- hatch.linked_dropship = src
+
+ RegisterSignal(src, COMSIG_DROPSHIP_ADD_EQUIPMENT, PROC_REF(add_equipment))
+ RegisterSignal(src, COMSIG_DROPSHIP_REMOVE_EQUIPMENT, PROC_REF(remove_equipment))
/obj/docking_port/mobile/marine_dropship/Destroy(force)
. = ..()
qdel(door_control)
+ UnregisterSignal(src, COMSIG_DROPSHIP_ADD_EQUIPMENT)
+ UnregisterSignal(src, COMSIG_DROPSHIP_REMOVE_EQUIPMENT)
/obj/docking_port/mobile/marine_dropship/proc/send_for_flyby()
in_flyby = TRUE
var/obj/docking_port/stationary/dockedAt = get_docked()
SSshuttle.moveShuttle(src.id, dockedAt.id, TRUE)
+/obj/docking_port/mobile/marine_dropship/proc/add_equipment(obj/docking_port/mobile/marine_dropship/dropship, obj/structure/dropship_equipment/equipment)
+ SIGNAL_HANDLER
+ equipments += equipment
+
+/obj/docking_port/mobile/marine_dropship/proc/remove_equipment(obj/docking_port/mobile/marine_dropship/dropship, obj/structure/dropship_equipment/equipment)
+ SIGNAL_HANDLER
+ equipments -= equipment
+
/obj/docking_port/mobile/marine_dropship/proc/get_door_data()
return door_control.get_data()
@@ -118,6 +130,9 @@
dwidth = 4
dheight = 8
+/obj/docking_port/mobile/marine_dropship/midway/get_transit_path_type()
+ return /turf/open/space/transit/dropship/midway
+
/obj/docking_port/mobile/marine_dropship/upp
name = "Akademia Nauk"
id = DROPSHIP_UPP
@@ -136,13 +151,24 @@
dwidth = 4
dheight = 8
+/obj/docking_port/mobile/marine_dropship/cyclone/get_transit_path_type()
+ return /turf/open/space/transit/dropship/cyclone
+
/obj/docking_port/mobile/marine_dropship/alamo
name = "Alamo"
id = DROPSHIP_ALAMO
+ preferred_direction = SOUTH // If you are changing this, please update the dir of the path below as well
+
+/obj/docking_port/mobile/marine_dropship/alamo/get_transit_path_type()
+ return /turf/open/space/transit/dropship/alamo
/obj/docking_port/mobile/marine_dropship/normandy
name = "Normandy"
id = DROPSHIP_NORMANDY
+ preferred_direction = SOUTH // If you are changing this, please update the dir of the path below as well
+
+/obj/docking_port/mobile/marine_dropship/normandy/get_transit_path_type()
+ return /turf/open/space/transit/dropship/normandy
/obj/docking_port/mobile/marine_dropship/check()
. = ..()
diff --git a/code/modules/shuttle/shuttles/trijent_elevator.dm b/code/modules/shuttle/shuttles/trijent_elevator.dm
index 89d0800017..4e0e7fbced 100644
--- a/code/modules/shuttle/shuttles/trijent_elevator.dm
+++ b/code/modules/shuttle/shuttles/trijent_elevator.dm
@@ -27,6 +27,8 @@
door_control.label = "elevator"
for(var/area/shuttle_area in shuttle_areas)
for(var/obj/structure/machinery/door/door in shuttle_area)
+ if(istype(door, /obj/structure/machinery/door/poddoor/filler_object)) //poddoor filler was sneaking in
+ continue
door_control.add_door(door, door.id)
/obj/docking_port/mobile/trijent_elevator/Destroy(force, ...)
@@ -37,12 +39,6 @@
. = ..()
door_control.control_doors("force-lock-launch", "all", force=TRUE)
-/obj/docking_port/mobile/trijent_elevator/linkup(datum/map_template/shuttle/template, obj/docking_port/stationary/dock)
- ..()
- var/datum/map_template/shuttle/trijent_elevator/elev = template
- elevator_network = elev.elevator_network
- log_debug("Adding network [elev.elevator_network] to [id]")
-
/obj/docking_port/stationary/trijent_elevator
dir=NORTH
width=7
diff --git a/code/modules/tgui/tgui-say/modal.dm b/code/modules/tgui/tgui-say/modal.dm
index f1e87e001c..a57b907499 100644
--- a/code/modules/tgui/tgui-say/modal.dm
+++ b/code/modules/tgui/tgui-say/modal.dm
@@ -10,9 +10,7 @@
* string - A JSON encoded message to open the modal.
*/
/client/proc/tgui_say_create_open_command(channel)
- var/message = TGUI_CREATE_MESSAGE("open", list(
- channel = channel,
- ))
+ var/message = TGUI_CREATE_OPEN_MESSAGE(channel)
return "\".output tgui_say.browser:update [message]\""
/**
@@ -36,6 +34,7 @@
/datum/tgui_say/New(client/client, id)
src.client = client
window = new(client, id)
+ winset(client, "tgui_say", "size=1,1;is-visible=0;")
window.subscribe(src, PROC_REF(on_message))
window.is_browser = TRUE
@@ -62,12 +61,15 @@
*/
/datum/tgui_say/proc/load()
window_open = FALSE
- winshow(client, "tgui_say", FALSE)
+
+ winset(client, "tgui_say", "pos=700,500;size=380,30;is-visible=0;")
+
window.send_message("props", list(
lightMode = client.prefs?.tgui_say_light_mode,
maxLength = max_length,
- roles = client.admin_holder?.get_tgui_say_roles()
+ extraChannels = client.admin_holder?.get_tgui_say_extra_channels()
))
+
stop_thinking()
return TRUE
@@ -110,10 +112,10 @@
close()
return TRUE
if (type == "thinking")
- if(payload["mode"] == TRUE)
+ if(payload["visible"] == TRUE)
start_thinking()
return TRUE
- if(payload["mode"] == FALSE)
+ if(payload["visible"] == FALSE)
stop_thinking()
return TRUE
return FALSE
diff --git a/code/modules/tgui/tgui-say/typing.dm b/code/modules/tgui/tgui-say/typing.dm
index 3334ff4a34..e2d8e5fa2a 100644
--- a/code/modules/tgui/tgui-say/typing.dm
+++ b/code/modules/tgui/tgui-say/typing.dm
@@ -22,6 +22,7 @@
remove_all_indicators()
return ..()
+/// Whether or not to show a typing indicator when speaking. Defaults to on.
/client/verb/typing_indicator()
set name = "Show/Hide Typing Indicator"
set category = "Preferences.Chat"
diff --git a/code/modules/tgui/tgui.dm b/code/modules/tgui/tgui.dm
index c01452a5b4..96c96a45a7 100644
--- a/code/modules/tgui/tgui.dm
+++ b/code/modules/tgui/tgui.dm
@@ -11,7 +11,7 @@
var/mob/user
/// The object which owns the UI.
var/datum/src_object
- /// The title of te UI.
+ /// The title of the UI.
var/title
/// The window_id for browse() and onclose().
var/datum/tgui_window/window
diff --git a/code/modules/tgui/tgui_alert.dm b/code/modules/tgui/tgui_alert.dm
index ce606c7425..a3fa3c519f 100644
--- a/code/modules/tgui/tgui_alert.dm
+++ b/code/modules/tgui/tgui_alert.dm
@@ -8,8 +8,10 @@
* * title - The of the alert modal, shown on the top of the TGUI window.
* * buttons - The options that can be chosen by the user, each string is assigned a button on the UI.
* * timeout - The timeout of the alert, after which the modal will close and qdel itself. Set to zero for no timeout.
+ * * autofocus - The bool that controls if this alert should grab window focus.
+ * * ui_state - The TGUI UI state that will be returned in ui_state(). Default: always_state
*/
-/proc/tgui_alert(mob/user, message, title, list/buttons, timeout = 60 SECONDS)
+/proc/tgui_alert(mob/user, message = "", title, list/buttons = list("Ok"), timeout = 60 SECONDS, autofocus = TRUE, ui_state = GLOB.always_state)
if (!user)
user = usr
if (!istype(user))
@@ -17,8 +19,12 @@
var/client/client = user
user = client.mob
else
- return
- var/datum/tgui_modal/alert = new(user, message, title, buttons, timeout)
+ return null
+
+ if(isnull(user.client))
+ return null
+
+ var/datum/tgui_modal/alert = new(user, message, title, buttons, timeout, autofocus, ui_state)
alert.tgui_interact(user)
alert.wait()
if (alert)
@@ -36,8 +42,10 @@
* * buttons - The options that can be chosen by the user, each string is assigned a button on the UI.
* * callback - The callback to be invoked when a choice is made.
* * timeout - The timeout of the alert, after which the modal will close and qdel itself. Set to zero for no timeout.
+ * * autofocus - The bool that controls if this alert should grab window focus.
+ * * ui_state - The TGUI UI state that will be returned in ui_state(). Default: always_state
*/
-/proc/tgui_alert_async(mob/user, message, title, list/buttons, datum/callback/callback, timeout = 60 SECONDS)
+/proc/tgui_alert_async(mob/user, message = "", title, list/buttons = list("Ok"), datum/callback/callback, timeout = 60 SECONDS, autofocus = TRUE, ui_state = GLOB.always_state)
if (!user)
user = usr
if (!istype(user))
@@ -45,8 +53,12 @@
var/client/client = user
user = client.mob
else
- return
- var/datum/tgui_modal/async/alert = new(user, message, title, buttons, callback, timeout)
+ return null
+
+ if(isnull(user.client))
+ return null
+
+ var/datum/tgui_modal/async/alert = new(user, message, title, buttons, callback, timeout, autofocus, ui_state)
alert.tgui_interact(user)
/**
@@ -68,13 +80,19 @@
var/start_time
/// The lifespan of the tgui_modal, after which the window will close and delete itself.
var/timeout
+ /// The bool that controls if this modal should grab window focus
+ var/autofocus
/// Boolean field describing if the tgui_modal was closed by the user.
var/closed
+ /// The TGUI UI state that will be returned in ui_state(). Default: always_state
+ var/datum/ui_state/state
-/datum/tgui_modal/New(mob/user, message, title, list/buttons, timeout)
- src.title = title
- src.message = message
+/datum/tgui_modal/New(mob/user, message, title, list/buttons, timeout, autofocus, ui_state)
+ src.autofocus = autofocus
src.buttons = buttons.Copy()
+ src.message = message
+ src.title = title
+ src.state = ui_state
if (timeout)
src.timeout = timeout
start_time = world.time
@@ -82,15 +100,16 @@
/datum/tgui_modal/Destroy(force, ...)
SStgui.close_uis(src)
- buttons = null
- . = ..()
+ state = null
+ buttons = null // TG QDEL_NULLs this
+ return ..()
/**
* Waits for a user's response to the tgui_modal's prompt before returning. Returns early if
* the window was closed by the user.
*/
/datum/tgui_modal/proc/wait()
- while (!choice && !closed)
+ while (!choice && !closed && !QDELETED(src))
stoplag(0.2 SECONDS)
/datum/tgui_modal/tgui_interact(mob/user, datum/tgui/ui)
@@ -104,30 +123,44 @@
closed = TRUE
/datum/tgui_modal/ui_state(mob/user)
- return GLOB.always_state
+ return state
+
+/datum/tgui_modal/ui_static_data(mob/user)
+ var/list/data = list()
+ data["autofocus"] = autofocus
+ data["buttons"] = buttons
+ data["message"] = message
+ data["large_buttons"] = FALSE // Pref?
+ data["swapped_buttons"] = FALSE // Pref?
+ data["title"] = title
+ return data
/datum/tgui_modal/ui_data(mob/user)
- . = list(
- "title" = title,
- "message" = message,
- "buttons" = buttons
- )
-
+ var/list/data = list()
if(timeout)
- .["timeout"] = clamp((timeout - (world.time - start_time) - 1 SECONDS) / (timeout - 1 SECONDS), 0, 1)
+ data["timeout"] = CLAMP01((timeout - (world.time - start_time) - 1 SECONDS) / (timeout - 1 SECONDS))
+ return data
-/datum/tgui_modal/ui_act(action, list/params)
+/datum/tgui_modal/ui_act(action, list/params, datum/tgui/ui, datum/ui_state/state)
. = ..()
if (.)
return
switch(action)
if("choose")
if (!(params["choice"] in buttons))
- return
- choice = params["choice"]
+ CRASH("[ui.user] entered a non-existent button choice: [params["choice"]]")
+ set_choice(params["choice"])
+ closed = TRUE
+ SStgui.close_uis(src)
+ return TRUE
+ if("cancel")
+ closed = TRUE
SStgui.close_uis(src)
return TRUE
+/datum/tgui_modal/proc/set_choice(choice)
+ src.choice = choice
+
/**
* # async tgui_modal
*
@@ -137,8 +170,8 @@
/// The callback to be invoked by the tgui_modal upon having a choice made.
var/datum/callback/callback
-/datum/tgui_modal/async/New(mob/user, message, title, list/buttons, callback, timeout)
- ..(user, title, message, buttons, timeout)
+/datum/tgui_modal/async/New(mob/user, message, title, list/buttons, callback, timeout, autofocus, ui_state)
+ ..(user, title, message, buttons, timeout, autofocus, ui_state)
src.callback = callback
/datum/tgui_modal/async/Destroy(force, ...)
@@ -149,12 +182,10 @@
. = ..()
qdel(src)
-/datum/tgui_modal/async/ui_act(action, list/params)
+/datum/tgui_modal/async/set_choice(choice)
. = ..()
- if (!. || choice == null)
- return
- callback.InvokeAsync(choice)
- qdel(src)
+ if(!isnull(src.choice))
+ callback?.InvokeAsync(src.choice)
/datum/tgui_modal/async/wait()
return
diff --git a/code/modules/tgui/tgui_input_list.dm b/code/modules/tgui/tgui_input_list.dm
index 8088ba5ffb..ae15cbf621 100644
--- a/code/modules/tgui/tgui_input_list.dm
+++ b/code/modules/tgui/tgui_input_list.dm
@@ -1,3 +1,21 @@
+/* Copyright 2020 bobbahbrown (https://github.com/bobbahbrown), watermelon914 (https://github.com/watermelon914)
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy of this software
+ * and associated documentation files (the "Software"), to deal in the Software without restriction,
+ * including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so,
+ * subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+ * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+ * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ */
+
/**
* Creates a TGUI input list window and returns the user's response.
*
@@ -5,15 +23,18 @@
* Arguments:
* * user - The user to show the input box to.
* * message - The content of the input box, shown in the body of the TGUI window.
- * * title - The title of the input box, shown on the top of the TGUI window.
- * * items - The options that can be chosen by the user, each string is assigned a button on the UI.
+ * * title - The title of the list input, shown on the top of the TGUI window.
+ * * buttons - The options that can be chosen by the user, each string is assigned a button on the UI.
+ * * timeout - The timeout of the alert, after which the list input will close and qdel itself. Set to zero for no timeout.
+ * * theme - The ui theme to use for the TGUI window.
* * default - If an option is already preselected on the UI. Current values, etc.
- * * timeout - The timeout of the input box, after which the menu will close and qdel itself. Set to zero for no timeout.
+ * * ui_state - The TGUI UI state that will be returned in ui_state(). Default: always_state
*/
-/proc/tgui_input_list(mob/user, message, title = "Select", list/items, timeout = 0, theme = null, ui_state = GLOB.always_state)
+/proc/tgui_input_list(mob/user, message, title = "Select", list/buttons, timeout = 0, theme = null, default, ui_state = GLOB.always_state)
if (!user)
user = usr
- if(!length(items))
+ if(!length(buttons))
+ stack_trace("tgui_input_list called with no buttons!")
return null
if (!istype(user))
if (istype(user, /client))
@@ -25,7 +46,7 @@
if(isnull(user.client))
return null
- var/datum/tgui_list_input/input = new(user, message, title, items, timeout, theme, ui_state)
+ var/datum/tgui_list_input/input = new(user, message, title, buttons, timeout, theme, default, ui_state)
if(input.invalid)
qdel(input)
return
@@ -35,6 +56,42 @@
. = input.choice
qdel(input)
+/**
+ * Creates an asynchronous TGUI input list window with an associated callback.
+ *
+ * This proc should be used to create inputs that invoke a callback with the user's chosen option.
+ * Arguments:
+ * * user - The user to show the input box to.
+ * * message - The content of the input box, shown in the body of the TGUI window.
+ * * title - The title of the list input, shown on the top of the TGUI window.
+ * * buttons - The options that can be chosen by the user, each string is assigned a button on the UI.
+ * * callback - The callback to be invoked when a choice is made.
+ * * timeout - The timeout of the alert, after which the list_input will close and qdel itself. Set to zero for no timeout.
+ * * theme - The ui theme to use for the TGUI window.
+ * * default - If an option is already preselected on the UI. Current values, etc.
+ * * ui_state - The TGUI UI state that will be returned in ui_state(). Default: always_state
+ */
+/proc/tgui_input_list_async(mob/user, message, title = "Select", list/buttons, datum/callback/callback, timeout = 60 SECONDS, theme = null, default, ui_state = GLOB.always_state)
+ if (!user)
+ user = usr
+ if(!length(buttons))
+ return null
+ if (!istype(user))
+ if (istype(user, /client))
+ var/client/client = user
+ user = client.mob
+ else
+ return null
+
+ if(isnull(user.client))
+ return null
+
+ var/datum/tgui_list_input/async/input = new(user, message, title, buttons, callback, timeout, theme, default, ui_state)
+ if(input.invalid)
+ qdel(input)
+ return
+ input.tgui_interact(user)
+
/**
* # tgui_list_input
*
@@ -46,10 +103,10 @@
var/title
/// The textual body of the TGUI window
var/message
- /// The list of items (responses) provided on the TGUI window
- var/list/items
+ /// The list of buttons (responses) provided on the TGUI window. These will automatically all be strings
+ var/list/buttons
/// Buttons (strings specifically) mapped to the actual value (e.g. a mob or a verb)
- var/list/items_map
+ var/list/buttons_map
/// The button that the user has pressed, null if no selection has been made
var/choice
/// The default button to be selected
@@ -62,42 +119,43 @@
var/closed
/// The TGUI UI state that will be returned in ui_state(). Default: always_state
var/datum/ui_state/state
+ /// String field for the theme to use
+ var/ui_theme
/// Whether the tgui list input is invalid or not (i.e. due to all list entries being null)
var/invalid = FALSE
- /// The theme that this UI should display
- var/theme
-/datum/tgui_list_input/New(mob/user, message, title, list/items, timeout, theme, ui_state)
+/datum/tgui_list_input/New(mob/user, message, title, list/buttons, timeout, theme = null, default, ui_state)
src.title = title
src.message = message
- src.items = list()
- src.items_map = list()
+ src.buttons = list()
+ src.buttons_map = list()
src.default = default
src.state = ui_state
- src.theme = theme
+ src.ui_theme = theme
var/list/repeat_items = list()
// Gets rid of illegal characters
var/static/regex/whitelistedWords = regex(@{"([^\u0020-\u8000]+)"})
- for(var/i in items)
+
+ for(var/i in buttons)
if(!i)
continue
var/string_key = whitelistedWords.Replace("[i]", "")
//avoids duplicated keys E.g: when areas have the same name
string_key = avoid_assoc_duplicate_keys(string_key, repeat_items)
- src.items += string_key
- src.items_map[string_key] = i
+ src.buttons += string_key
+ src.buttons_map[string_key] = i
- if(length(src.items) == 0)
+ if(length(src.buttons) == 0)
invalid = TRUE
if (timeout)
src.timeout = timeout
start_time = world.time
QDEL_IN(src, timeout)
-/datum/tgui_list_input/Destroy(force)
+/datum/tgui_list_input/Destroy(force, ...)
SStgui.close_uis(src)
state = null
- QDEL_NULL(items)
+ buttons = null // TG QDEL_NULLs this
return ..()
/**
@@ -106,12 +164,12 @@
*/
/datum/tgui_list_input/proc/wait()
while (!choice && !closed)
- stoplag(1)
+ stoplag(0.2 SECONDS)
/datum/tgui_list_input/tgui_interact(mob/user, datum/tgui/ui)
ui = SStgui.try_update_ui(user, src, ui)
if(!ui)
- ui = new(user, src, "ListInputModal")
+ ui = new(user, src, "ListInputWindow")
ui.open()
/datum/tgui_list_input/ui_close(mob/user)
@@ -123,11 +181,13 @@
/datum/tgui_list_input/ui_static_data(mob/user)
var/list/data = list()
- data["init_value"] = default || items[1]
- data["items"] = items
+ data["init_value"] = default || buttons[1]
+ data["items"] = buttons
+ data["large_buttons"] = FALSE // Pref?
data["message"] = message
+ data["swapped_buttons"] = FALSE // Pref?
data["title"] = title
- data["theme"] = theme
+ data["theme"] = ui_theme
return data
/datum/tgui_list_input/ui_data(mob/user)
@@ -136,15 +196,15 @@
data["timeout"] = clamp((timeout - (world.time - start_time) - 1 SECONDS) / (timeout - 1 SECONDS), 0, 1)
return data
-/datum/tgui_list_input/ui_act(action, list/params)
+/datum/tgui_list_input/ui_act(action, list/params, datum/tgui/ui, datum/ui_state/state)
. = ..()
if (.)
return
switch(action)
if("submit")
- if (!(params["entry"] in items))
+ if (!(params["entry"] in buttons))
return
- set_choice(items_map[params["entry"]])
+ set_choice(buttons_map[params["entry"]])
closed = TRUE
SStgui.close_uis(src)
return TRUE
@@ -155,3 +215,32 @@
/datum/tgui_list_input/proc/set_choice(choice)
src.choice = choice
+
+/**
+ * # async tgui_list_input
+ *
+ * An asynchronous version of tgui_list_input to be used with callbacks instead of waiting on user responses.
+ */
+/datum/tgui_list_input/async
+ /// The callback to be invoked by the tgui_modal upon having a choice made.
+ var/datum/callback/callback
+
+/datum/tgui_list_input/async/New(mob/user, message, title, list/buttons, callback, timeout, theme = null, default, ui_state)
+ ..(user, title, message, buttons, timeout, theme, default)
+ src.callback = callback
+
+/datum/tgui_list_input/async/Destroy(force, ...)
+ QDEL_NULL(callback)
+ . = ..()
+
+/datum/tgui_list_input/async/ui_close(mob/user)
+ . = ..()
+ qdel(src)
+
+/datum/tgui_list_input/async/set_choice(choice)
+ . = ..()
+ if(!isnull(src.choice))
+ callback?.InvokeAsync(src.choice)
+
+/datum/tgui_list_input/async/wait()
+ return
diff --git a/code/modules/tgui/tgui_number_input.dm b/code/modules/tgui/tgui_number_input.dm
index aa189b1d20..56956cb936 100644
--- a/code/modules/tgui/tgui_number_input.dm
+++ b/code/modules/tgui/tgui_number_input.dm
@@ -13,8 +13,10 @@
* * max_value - Specifies a maximum value. If none is set, any number can be entered. Pressing "max" defaults to 1000.
* * min_value - Specifies a minimum value. Often 0.
* * timeout - The timeout of the number input, after which the modal will close and qdel itself. Set to zero for no timeout.
+ * * integer_only - whether the inputted number is rounded down into an integer.
+ * * ui_state - The TGUI UI state that will be returned in ui_state(). Default: always_state
*/
-/proc/tgui_input_number(mob/user, message, title = "Number Input", default = 0, max_value = 10000, min_value = 0, timeout = 0, integer_only = TRUE)
+/proc/tgui_input_number(mob/user, message, title = "Number Input", default = 0, max_value = 10000, min_value = 0, timeout = 0, integer_only = TRUE, ui_state = GLOB.always_state)
if (!user)
user = usr
if (!istype(user))
@@ -22,8 +24,12 @@
var/client/client = user
user = client.mob
else
- return
- var/datum/tgui_input_number/number_input = new(user, message, title, default, max_value, min_value, timeout, integer_only)
+ return null
+
+ if (isnull(user.client))
+ return null
+
+ var/datum/tgui_input_number/number_input = new(user, message, title, default, max_value, min_value, timeout, integer_only, ui_state)
number_input.tgui_interact(user)
number_input.wait()
if (number_input)
@@ -31,8 +37,9 @@
qdel(number_input)
///A clone of tgui_input_number that defaults to accepting negative inputs too.
-/proc/tgui_input_real_number(mob/user, message, title = "Number Input", default = 0, max_value = SHORT_REAL_LIMIT, min_value = -SHORT_REAL_LIMIT, timeout = 0, integer_only = FALSE)
- return tgui_input_number(user, message, title, default, max_value, min_value, timeout, integer_only)
+/proc/tgui_input_real_number(mob/user, message, title = "Number Input", default = 0, max_value = SHORT_REAL_LIMIT, min_value = -SHORT_REAL_LIMIT, timeout = 0, integer_only = FALSE, ui_state = GLOB.always_state)
+ return tgui_input_number(user, message, title, default, max_value, min_value, timeout, integer_only, ui_state)
+
/**
* Creates an asynchronous TGUI number input window with an associated callback.
*
@@ -47,8 +54,10 @@
* * min_value - Specifies a minimum value. Often 0.
* * callback - The callback to be invoked when a choice is made.
* * timeout - The timeout of the number input, after which the modal will close and qdel itself. Set to zero for no timeout.
+ * * integer_only - whether the inputted number is rounded down into an integer.
+ * * ui_state - The TGUI UI state that will be returned in ui_state(). Default: always_state
*/
-/proc/tgui_input_number_async(mob/user, message, title = "Number Input", default = 0, max_value = 10000, min_value = 0, datum/callback/callback, timeout = 60 SECONDS)
+/proc/tgui_input_number_async(mob/user, message, title = "Number Input", default = 0, max_value = 10000, min_value = 0, datum/callback/callback, timeout = 60 SECONDS, integer_only = TRUE, ui_state = GLOB.always_state)
if (!user)
user = usr
if (!istype(user))
@@ -57,7 +66,7 @@
user = client.mob
else
return
- var/datum/tgui_input_number/async/number_input = new(user, message, title, default, max_value, min_value, callback, timeout)
+ var/datum/tgui_input_number/async/number_input = new(user, message, title, default, max_value, min_value, callback, timeout, integer_only, ui_state)
number_input.tgui_interact(user)
/**
@@ -79,23 +88,25 @@
var/message
/// The minimum value that can be entered.
var/min_value
+ /// If the final value will be rounded
+ var/integer_only
/// The time at which the number input was created, for displaying timeout progress.
var/start_time
/// The lifespan of the number input, after which the window will close and delete itself.
var/timeout
/// The title of the TGUI window
var/title
- /// If the final value will be rounded
- var/integer_only
+ /// The TGUI UI state that will be returned in ui_state(). Default: always_state
+ var/datum/ui_state/state
-
-/datum/tgui_input_number/New(mob/user, message, title, default, max_value, min_value, timeout, integer_only)
+/datum/tgui_input_number/New(mob/user, message, title, default, max_value, min_value, timeout, integer_only, ui_state)
src.default = default
src.max_value = max_value
src.message = message
src.min_value = min_value
src.title = title
src.integer_only = integer_only
+ src.state = ui_state
if (timeout)
src.timeout = timeout
start_time = world.time
@@ -113,7 +124,8 @@
/datum/tgui_input_number/Destroy(force, ...)
SStgui.close_uis(src)
- . = ..()
+ state = null
+ return ..()
/**
* Waits for a user's response to the tgui_input_number's prompt before returning. Returns early if
@@ -135,38 +147,43 @@
closed = TRUE
/datum/tgui_input_number/ui_state(mob/user)
- return GLOB.always_state
+ return state
/datum/tgui_input_number/ui_static_data(mob/user)
- . = list(
- "init_value" = default, // Default is a reserved keyword
- "max_value" = max_value,
- "message" = message,
- "min_value" = min_value,
- "preferences" = list(),
- "title" = title,
- )
+ var/list/data = list()
+ data["init_value"] = default // Default is a reserved keyword
+ data["large_buttons"] = FALSE // Pref?
+ data["max_value"] = max_value
+ data["message"] = message
+ data["min_value"] = min_value
+ data["swapped_buttons"] = FALSE // Pref?
+ data["title"] = title
+ data["round_value"] = integer_only
+ return data
/datum/tgui_input_number/ui_data(mob/user)
- . = list()
+ var/list/data = list()
if(timeout)
- .["timeout"] = CLAMP01((timeout - (world.time - start_time) - 1 SECONDS) / (timeout - 1 SECONDS))
+ data["timeout"] = CLAMP01((timeout - (world.time - start_time) - 1 SECONDS) / (timeout - 1 SECONDS))
+ return data
-/datum/tgui_input_number/ui_act(action, list/params)
+/datum/tgui_input_number/ui_act(action, list/params, datum/tgui/ui, datum/ui_state/state)
. = ..()
if (.)
return
switch(action)
if("submit")
- if(!isnum(params["entry"]))
- CRASH("A non number was input into tgui input number by [usr]")
var/choice = params["entry"]
+ if(!isnum(choice))
+ CRASH("A non number was input into tgui input number by [ui.user]")
+ if(choice != choice) //isnan
+ CRASH("A NaN was input into tgui input number by [ui.user]")
if(integer_only)
- choice = round(params["entry"])
+ choice = round(choice)
if(choice > max_value)
- CRASH("A number greater than the max value was input into tgui input number by [usr]")
+ CRASH("A number greater than the max value was input into tgui input number by [ui.user]")
if(choice < min_value)
- CRASH("A number less than the min value was input into tgui input number by [usr]")
+ CRASH("A number less than the min value was input into tgui input number by [ui.user]")
set_entry(choice)
closed = TRUE
SStgui.close_uis(src)
@@ -177,7 +194,7 @@
return TRUE
/datum/tgui_input_number/proc/set_entry(entry)
- src.entry = entry
+ src.entry = entry
/**
* # async tgui_input_number
@@ -188,8 +205,8 @@
/// The callback to be invoked by the tgui_input_number upon having a choice made.
var/datum/callback/callback
-/datum/tgui_input_number/async/New(mob/user, message, title, default, max_value, min_value, callback, timeout)
- ..(user, message, title, default, max_value, min_value, timeout)
+/datum/tgui_input_number/async/New(mob/user, message, title, default, max_value, min_value, callback, timeout, integer_only, ui_state)
+ ..(user, message, title, default, max_value, min_value, timeout, integer_only, ui_state)
src.callback = callback
/datum/tgui_input_number/async/Destroy(force, ...)
diff --git a/code/modules/tgui/tgui_window.dm b/code/modules/tgui/tgui_window.dm
index 21f4a8ba94..987a2aca92 100644
--- a/code/modules/tgui/tgui_window.dm
+++ b/code/modules/tgui/tgui_window.dm
@@ -26,6 +26,18 @@
var/initial_inline_css
var/mouse_event_macro_set = FALSE
+ /**
+ * Static list used to map in macros that will then emit execute events to the tgui window
+ * A small disclaimer though I'm no tech wiz: I don't think it's possible to map in right or middle
+ * clicks in the current state, as they're keywords rather than modifiers.
+ */
+ var/static/list/byondToTguiEventMap = list(
+ "MouseDown" = "byond/mousedown",
+ "MouseUp" = "byond/mouseup",
+ "Ctrl" = "byond/ctrldown",
+ "Ctrl+UP" = "byond/ctrlup",
+ )
+
/**
* public
*
@@ -382,11 +394,6 @@
if(mouse_event_macro_set)
return
- var/list/byondToTguiEventMap = list(
- "MouseDown" = "byond/mousedown",
- "MouseUp" = "byond/mouseup"
- )
-
for(var/mouseMacro in byondToTguiEventMap)
var/command_template = ".output CONTROL PAYLOAD"
var/event_message = TGUI_CREATE_MESSAGE(byondToTguiEventMap[mouseMacro], null)
@@ -404,14 +411,9 @@
winset(client, "[mouseMacro]Window[id]Macro", params)
mouse_event_macro_set = TRUE
-
/datum/tgui_window/proc/remove_mouse_macro()
if(!mouse_event_macro_set)
stack_trace("Unsetting mouse macro on tgui window that has none")
- var/list/byondToTguiEventMap = list(
- "MouseDown" = "byond/mousedown",
- "MouseUp" = "byond/mouseup"
- )
for(var/mouseMacro in byondToTguiEventMap)
winset(client, null, "[mouseMacro]Window[id]Macro.parent=null")
mouse_event_macro_set = FALSE
diff --git a/code/modules/tgui_input/checkboxes.dm b/code/modules/tgui_input/checkboxes.dm
new file mode 100644
index 0000000000..fa90ee51d7
--- /dev/null
+++ b/code/modules/tgui_input/checkboxes.dm
@@ -0,0 +1,211 @@
+/**
+ * ### tgui_input_checkbox
+ * Opens a window with a list of checkboxes and returns a list of selected choices.
+ *
+ * * Arguments:
+ * * user - The mob to display the window to
+ * * message - The message inside the window
+ * * title - The title of the window
+ * * list/items - The list of items to display
+ * * min_checked - The minimum number of checkboxes that must be checked (defaults to 1)
+ * * max_checked - The maximum number of checkboxes that can be checked (optional)
+ * * timeout - The timeout for the input (optional)
+ * * theme - The ui theme to use for the TGUI window (optional).
+ * * ui_state - The TGUI UI state that will be returned in ui_state(). Default: always_state
+ */
+/proc/tgui_input_checkboxes(mob/user, message, title = "Select", list/items, min_checked = 1, max_checked = 50, timeout = 0, theme = null, ui_state = GLOB.always_state)
+ if (!user)
+ user = usr
+ if(!length(items))
+ return null
+ if (!istype(user))
+ if (istype(user, /client))
+ var/client/client = user
+ user = client.mob
+ else
+ return null
+
+ if(isnull(user.client))
+ return null
+
+ var/datum/tgui_checkbox_input/input = new(user, message, title, items, min_checked, max_checked, timeout, theme, ui_state)
+ input.tgui_interact(user)
+ input.wait()
+ if (input)
+ . = input.choices
+ qdel(input)
+
+/**
+ * ### tgui_input_checkbox
+ * Opens a window with a list of checkboxes and returns a list of selected choices.
+ *
+ * * Arguments:
+ * * user - The mob to display the window to
+ * * message - The message inside the window
+ * * title - The title of the window
+ * * list/items - The list of items to display
+ * * min_checked - The minimum number of checkboxes that must be checked (defaults to 1)
+ * * max_checked - The maximum number of checkboxes that can be checked (optional)
+ * * callback - The callback to be invoked when a choice is made.
+ * * timeout - The timeout for the input (optional)
+ * * theme - The ui theme to use for the TGUI window (optional).
+ * * ui_state - The TGUI UI state that will be returned in ui_state(). Default: always_state
+ */
+/proc/tgui_input_checkboxes_async(mob/user, message, title = "Select", list/items, min_checked = 1, max_checked = 50, datum/callback/callback, timeout = 0, theme = null, ui_state = GLOB.always_state)
+ if (!user)
+ user = usr
+ if(!length(items))
+ return null
+ if (!istype(user))
+ if (istype(user, /client))
+ var/client/client = user
+ user = client.mob
+ else
+ return null
+
+ if(isnull(user.client))
+ return null
+
+ var/datum/tgui_checkbox_input/async/input = new(user, message, title, items, min_checked, max_checked, callback, timeout, theme, ui_state)
+ input.tgui_interact(user)
+
+/// Window for tgui_input_checkboxes
+/datum/tgui_checkbox_input
+ /// Title of the window
+ var/title
+ /// Message to display
+ var/message
+ /// List of items to display
+ var/list/items
+ /// List of selected items
+ var/list/choices
+ /// Time when the input was created
+ var/start_time
+ /// Timeout for the input
+ var/timeout
+ /// Whether the input was closed
+ var/closed
+ /// Minimum number of checkboxes that must be checked
+ var/min_checked
+ /// Maximum number of checkboxes that can be checked
+ var/max_checked
+ /// The TGUI UI state that will be returned in ui_state(). Default: always_state
+ var/datum/ui_state/state
+ /// String field for the theme to use
+ var/ui_theme
+
+/datum/tgui_checkbox_input/New(mob/user, message, title, list/items, min_checked, max_checked, timeout, theme = null, ui_state)
+ src.title = title
+ src.message = message
+ src.items = items.Copy()
+ src.min_checked = min_checked
+ src.max_checked = max_checked
+ src.state = ui_state
+ src.ui_theme = theme
+
+ if (timeout)
+ src.timeout = timeout
+ start_time = world.time
+ QDEL_IN(src, timeout)
+
+/datum/tgui_checkbox_input/Destroy(force)
+ SStgui.close_uis(src)
+ state = null
+ items = null // TG QDEL_NULLs this
+ return ..()
+
+/**
+ * Waits for a user's response to the tgui_checkbox_input's prompt before returning. Returns early if
+ * the window was closed by the user.
+ */
+/datum/tgui_checkbox_input/proc/wait()
+ while (!closed && !QDELETED(src))
+ stoplag(0.2 SECONDS)
+
+/datum/tgui_checkbox_input/tgui_interact(mob/user, datum/tgui/ui)
+ ui = SStgui.try_update_ui(user, src, ui)
+ if(!ui)
+ ui = new(user, src, "CheckboxInput")
+ ui.open()
+
+/datum/tgui_checkbox_input/ui_close(mob/user)
+ . = ..()
+ closed = TRUE
+
+/datum/tgui_checkbox_input/ui_state(mob/user)
+ return state
+
+/datum/tgui_checkbox_input/ui_data(mob/user)
+ var/list/data = list()
+
+ if(timeout)
+ data["timeout"] = CLAMP01((timeout - (world.time - start_time) - 1 SECONDS) / (timeout - 1 SECONDS))
+
+ return data
+
+/datum/tgui_checkbox_input/ui_static_data(mob/user)
+ var/list/data = list()
+
+ data["items"] = items
+ data["min_checked"] = min_checked
+ data["max_checked"] = max_checked
+ data["large_buttons"] = TRUE // Pref?
+ data["message"] = message
+ data["swapped_buttons"] = FALSE // Pref?
+ data["title"] = title
+ data["theme"] = ui_theme
+
+ return data
+
+/datum/tgui_checkbox_input/ui_act(action, list/params, datum/tgui/ui, datum/ui_state/state)
+ . = ..()
+ if (.)
+ return
+
+ switch(action)
+ if("submit")
+ var/list/selections = params["entry"]
+ if(length(selections) >= min_checked && length(selections) <= max_checked)
+ set_choices(selections)
+ closed = TRUE
+ SStgui.close_uis(src)
+ return TRUE
+
+ if("cancel")
+ closed = TRUE
+ SStgui.close_uis(src)
+ return TRUE
+
+ return FALSE
+
+/datum/tgui_checkbox_input/proc/set_choices(list/selections)
+ src.choices = selections.Copy()
+
+/**
+ * # async tgui_checkbox_input
+ *
+ * An asynchronous version of tgui_checkbox_input to be used with callbacks instead of waiting on user responses.
+ */
+/datum/tgui_checkbox_input/async
+ /// The callback to be invoked by the tgui_modal upon having a choice made.
+ var/datum/callback/callback
+
+/datum/tgui_checkbox_input/async/New(mob/user, message, title, list/items, min_checked, max_checked, callback, timeout, theme = null, ui_state)
+ ..(user, message, title, items, min_checked, max_checked, callback, timeout, theme, ui_state)
+ src.callback = callback
+
+/datum/tgui_checkbox_input/async/Destroy(force, ...)
+ QDEL_NULL(callback)
+ . = ..()
+
+/datum/tgui_checkbox_input/async/ui_close(mob/user)
+ . = ..()
+ qdel(src)
+
+/datum/tgui_checkbox_input/async/set_choices(list/selections)
+ . = ..()
+ if(length(choices))
+ callback?.InvokeAsync(choices)
+
+/datum/tgui_checkbox_input/async/wait()
+ return
diff --git a/code/modules/tgui_input/text.dm b/code/modules/tgui_input/text.dm
index 2a9b7f6dff..15c50554bb 100644
--- a/code/modules/tgui_input/text.dm
+++ b/code/modules/tgui_input/text.dm
@@ -14,8 +14,10 @@
* * multiline - Bool that determines if the input box is much larger. Good for large messages, laws, etc.
* * encode - Toggling this determines if input is filtered via html_encode. Setting this to FALSE gives raw input.
* * timeout - The timeout of the textbox, after which the modal will close and qdel itself. Set to zero for no timeout.
+ * * trim - Whether or not to trim leading and trailing whitespace from your input. Defaults to TRUE
+ * * ui_state - The TGUI UI state that will be returned in ui_state(). Default: always_state
*/
-/proc/tgui_input_text(mob/user, message = "", title = "Text Input", default, max_length = MAX_MESSAGE_LEN, multiline = FALSE, encode = TRUE, timeout = 0)
+/proc/tgui_input_text(mob/user, message = "", title = "Text Input", default, max_length = MAX_MESSAGE_LEN, multiline = FALSE, encode = TRUE, timeout = 0, trim = TRUE, ui_state = GLOB.always_state)
if (!user)
user = usr
if (!istype(user))
@@ -23,7 +25,11 @@
var/client/client = user
user = client.mob
else
- return
+ return null
+
+ if(isnull(user.client))
+ return null
+
// Client does NOT have tgui_input on: Returns regular input
/*
if(!user.client.prefs.read_preference(/datum/preference/toggle/tgui_input))
@@ -39,7 +45,7 @@
return input(user, message, title, default) as text|null
*/
- var/datum/tgui_input_text/text_input = new(user, message, title, default, max_length, multiline, encode, timeout)
+ var/datum/tgui_input_text/text_input = new(user, message, title, default, max_length, multiline, encode, timeout, trim, ui_state)
text_input.tgui_interact(user)
text_input.wait()
if (text_input)
@@ -73,14 +79,20 @@
var/timeout
/// The title of the TGUI window
var/title
+ /// The TGUI UI state that will be returned in ui_state(). Default: always_state
+ var/datum/ui_state/state
+ /// Whether to trim leading and trailing spaces
+ var/trim
-/datum/tgui_input_text/New(mob/user, message, title, default, max_length, multiline, encode, timeout)
+/datum/tgui_input_text/New(mob/user, message, title, default, max_length, multiline, encode, timeout, trim, ui_state)
src.default = default
src.encode = encode
src.max_length = max_length
src.message = message
src.multiline = multiline
src.title = title
+ src.state = ui_state
+ src.trim = trim
if (timeout)
src.timeout = timeout
start_time = world.time
@@ -88,6 +100,7 @@
/datum/tgui_input_text/Destroy(force, ...)
SStgui.close_uis(src)
+ state = null
return ..()
/**
@@ -109,7 +122,7 @@
closed = TRUE
/datum/tgui_input_text/ui_state(mob/user)
- return GLOB.always_state
+ return state
/datum/tgui_input_text/ui_static_data(mob/user)
var/list/data = list()
@@ -141,7 +154,7 @@
CRASH("[usr] typed a text string longer than the max length")
if(encode && (length(html_encode(params["entry"])) > max_length))
to_chat(usr, SPAN_NOTICE("Your message was clipped due to special character usage."))
- set_entry(params["entry"])
+ set_entry(params["entry"], trim)
closed = TRUE
SStgui.close_uis(src)
return TRUE
@@ -156,7 +169,10 @@
* This can sometimes result in a string that is longer than the max length.
* If the string is longer than the max length, it will be clipped.
*/
-/datum/tgui_input_text/proc/set_entry(entry)
+/datum/tgui_input_text/proc/set_entry(entry, trim)
if(!isnull(entry))
var/converted_entry = encode ? html_encode(entry) : entry
- src.entry = trim(converted_entry, max_length)
+ if(trim)
+ src.entry = trim(converted_entry)
+ else
+ src.entry = converted_entry
diff --git a/code/modules/vehicles/interior/interior.dm b/code/modules/vehicles/interior/interior.dm
index f2afcd5ae5..8fb65602c9 100644
--- a/code/modules/vehicles/interior/interior.dm
+++ b/code/modules/vehicles/interior/interior.dm
@@ -304,21 +304,13 @@
// Returns min and max turfs for the interior
/datum/interior/proc/get_bound_turfs()
- var/turf/min = TURF_FROM_COORDS_LIST(reservation.bottom_left_coords)
- if(!min)
- return null
-
- var/turf/max = TURF_FROM_COORDS_LIST(reservation.top_right_coords)
- if(!max)
- return null
-
- return list(min, max)
+ return list(reservation.bottom_left_turfs[1], reservation.top_right_turfs[1])
/datum/interior/proc/get_middle_coords()
- var/turf/min = reservation.bottom_left_coords
- var/turf/max = reservation.top_right_coords
+ var/turf/min = reservation.bottom_left_turfs[1]
+ var/turf/max = reservation.top_right_turfs[1]
+ return list(Floor(min.x + (max.x - min.x)/2), Floor(min.y + (max.y - min.y)/2), min.z)
- return list(Floor(min[1] + (max[1] - min[1])/2), Floor(min[2] + (max[2] - min[2])/2), min[3])
/datum/interior/proc/get_middle_turf()
var/list/turf/bounds = get_bound_turfs()
diff --git a/colonialmarines.dme b/colonialmarines.dme
index 58cee33fcf..01c2e15a1a 100644
--- a/colonialmarines.dme
+++ b/colonialmarines.dme
@@ -109,6 +109,7 @@
#include "code\__DEFINES\tgui.dm"
#include "code\__DEFINES\traits.dm"
#include "code\__DEFINES\turf_flags.dm"
+#include "code\__DEFINES\turfs.dm"
#include "code\__DEFINES\unit_tests.dm"
#include "code\__DEFINES\urls.dm"
#include "code\__DEFINES\vehicle.dm"
@@ -170,6 +171,7 @@
#include "code\__HELPERS\sanitize_values.dm"
#include "code\__HELPERS\shell.dm"
#include "code\__HELPERS\status_effects.dm"
+#include "code\__HELPERS\string_lists.dm"
#include "code\__HELPERS\text.dm"
#include "code\__HELPERS\traits.dm"
#include "code\__HELPERS\type2type.dm"
@@ -720,6 +722,7 @@
#include "code\game\area\techtree.dm"
#include "code\game\area\varadero.dm"
#include "code\game\area\WhiskeyOutpost.dm"
+#include "code\game\camera_manager\camera_manager.dm"
#include "code\game\cas_manager\datums\cas_fire_envelope.dm"
#include "code\game\cas_manager\datums\cas_fire_mission.dm"
#include "code\game\cas_manager\datums\cas_iff_group.dm"
@@ -2378,6 +2381,7 @@
#include "code\modules\tgui\tgui-say\modal.dm"
#include "code\modules\tgui\tgui-say\speech.dm"
#include "code\modules\tgui\tgui-say\typing.dm"
+#include "code\modules\tgui_input\checkboxes.dm"
#include "code\modules\tgui_input\text.dm"
#include "code\modules\tgui_panel\audio.dm"
#include "code\modules\tgui_panel\external.dm"
diff --git a/dependencies.sh b/dependencies.sh
index 1934553cb0..7540707df5 100644
--- a/dependencies.sh
+++ b/dependencies.sh
@@ -1,4 +1,4 @@
-#!/bin/bash
+#!/bin/sh
#Project dependencies file
#Final authority on what's required to fully build the project
@@ -11,8 +11,8 @@ export BYOND_MINOR=1627
export RUST_G_VERSION=2.1.0
#node version
-export NODE_VERSION=14
-export NODE_VERSION_PRECISE=14.16.1
+export NODE_VERSION=20
+export NODE_VERSION_LTS=20.12.0
# SpacemanDMM git tag
export SPACEMAN_DMM_VERSION=suite-1.8
diff --git a/icons/effects/Targeted.dmi b/icons/effects/Targeted.dmi
index fce948574a3d82c5803aa847cecf8ea3e1b08fe8..24b82a90fe6ce2c6efa1401e341ce6dff509a9ac 100644
GIT binary patch
literal 7272
zcmbVxcUV*1n{6-xf>H#OA{bBsv4E%$iiD=p3@FkRLbXvt2Z0czhzb$}1f)m@=|vzC
z1O$Yrw9q?2sR>O@K@yTo@SERr@7%dF&ph+T$vN5Q-TUP1z1G_6ee?XLsR19a1TO#p
z;4?D3Y7PK!Nbem7xBvivNK{n>`$P?~u<^fo-_OO@)!W|{?gan@XJse%@JC({yf#ig
zD>0G({Ya2k{O1~+gY^FEuF+#S-37d@PkX_Yk=_Rnk}horPTlnvJ)SQ-3p(i=YkaYt
zkjmvAD<+WYRK<%wd7XbfX`v)BUHaX~xvL>Dy{E2spDC44RPMJl@6Z&m5Ph*&YChnV
zKcE~g&J8P(h1tgCg;dye;E(<=pUK;wupqqqSPB1Bt~n%O&GRzW#o%o|Yd5rlV&iKrW!v8p0a_1)S_UvSORF>jwwlnJ>P@Y!RRF-1KWDf00X~Jbb8ti)
zQ%qvQEcX68x_;SZH}&k9cAIsSn}{n
z2qoWoQQ=&U12f$kPv3Q65ih-FR66UZ;`|$!VPflk4Ub*p#$=-NMVk)SAj7C=2_Da`4UOj?uqS5KP504O3$(QuV=B2dXRNHh
z6mRJ3fDq)85NaNQ>0kGPb5`QX)Nhy<6`>
zG;NCE)bnU!$_J4DBceqyT!@{PHA$ilwT#+j?}DJ-d%8^K}Ca!Anjrlne6?j=)$Z4-g#mp2@cJovHWN
z^B&-rpn~E#NycrZjSbI@V9UdBKZwhMpYiInn%%SunpZDKoul-n0P1mhPy3P6(1@j0
z!cr}MGTh`gUm;wI>$@k5~1S;=8{#j&5BMx(iHD<#!wp(3%sp00-(Xk9}sI
ztkE9g+bW$d;TbZpmC~6w1glD`ll9uS>3a2B;WUQH&+V?`H1KT{QwHLaSt2zWYl~Hy
z449@JFKDGrwB1FXqsD@-68&zLI9MP2Qd#}0=;w9(<>!jSOs+iU?>~Gk4zP`_-7`-m
ze_HX1G#&OuUE|^InDta$7l`gDMKKJVSxcLZGxu=|-yGX+S}Wus(Kr<3nc_hL_Y^f~
zwG8QtK+GD=R3*>x(-m(Hx9aPH7#awfAn`reVz}z9kCZFi8>Y(SqQWy5R?YxvkIA;<
z5zZ%S^#UO_0yu*_@uE>$ws`L99`{SzF3G&f_Mb9s7qjZ@K$vIm-;1-S4NBK*
z)$E4;wzABw9xqYF7_laI+;-MIiq5j`!4wed*zolMcUCvYWYlq?nz=zm0hy6B;g%bL
z4-+bRe)g9$E}tu<)Pm@1)Gnq!6)u@*@t&CY*$+Ai&TO4d{VA5Qu9yl?I%mSqs5v)*
zVjxI_#Za`U`{kC#4<%HuGqp*?)KvE7%4Vytnl<2mT#bh)7!9zFL&qtv2rN6G^y
zDS<4JXzXpcdo5eRU9_n#$Z)!C%L|$7IdTX519X92wbEGzcWOX%6Zigh=F5L6v&lPT
zLlvYuB&kJMh##LMXq+l?m4{vsYN0RlQF$&oRW{9QXEMNhS=
zsqXq+Cj5YchYJz)Z;BrE$)`%F-na`27X{?F*QL0w1voc_;|2lN=;@Q?1{?88WqQzK-!`nFph8>#;F9ygHr`Jr+SC
z{Ft9wuUmVXIViB@W6U;2;TnwWkzOaWbaG&fk2K*(^Tz3<7sWotjS}hZSwif+F2|~k
zYI$g9sPe$o4C^_dpg!Y%-}*}B`(BvpmXA4m8E@AjS3LjVF6^Wrr_L55vD7}Oji&oh
z!`!4|$%vAcDKEVi`GUFy#pXFYq9u7L-2u8uxSz6DQ@xkl7eptf9hZ+`P^N2;AE{fP
zBlL(_i3YfeQ$nzzD)$pZgzyw`Yc3{DFtD5&hX0m;Du13mo)7
z+$o8S!?_F4v`7d|mh7YR{1Iu+ga}Sy!+xC>A8i_e(41^Nd+Fc9bEN(C)}OydUW9Kw
zZpao_ToK{1a=3W(OI`1D`BYui9?_7tbRQjHtvm%M;E}^0s9ppiIM*O=P!}S&E{^&u
zASj)6j@nc(B25gZQ{QvzjWH%=Z(E>!W(HD+52i*a6q^vr|Iv!Pex@$;!mscjx0Z&P
z$9#L031=nlB`=j0{*9QxcQNf%a+Udmf9pgcp&Rpd86|Ok`Itahsjm%;J%<>C#(0DW
z*mf4uI9<8+3kB0I9&LP>XM)moKn!(NjJdGX*0(I!=qE3kDclUV9fwYc24~(1WS$o=G^B9AGs?(M(wh_DYV`YJ*$lZvWuw*3gUecc
zc0vQ7W`9>R_rf0`H@$Fo-vrX)Uzt>Oum2+$PgZD)&6!`HyssO$P%>NO)EHjzT
zb%o>8FXcyi7q^bh+JXR2+p}>0rC)q`?s2>S=38&$_@srX!$
z1um#2spqP0!Ir}G5t}rC%PAwTKgIs)jp9O^$waxVX2>TSBZ=s-V?AeK2`#lt_?cnO
z_WEFdA0H*w$DUV7?fmalVW#(rJ4kNYj~(3@#m7GO6uKcOCes3>AK#h*&&AgxJc@aB
zAIiq}S%~$^NQHxA!fjHEKaQxAO(wLv+BbS?-a5V#KYBw;+9NRjQiu3v`s0|AeKetP=yVlVj=+BtOp
zp7VIP0A4ru65jtT1pl9`_&*0li61uj;>*ll87L=q#br~9iVd#ety!$rrVq$%co}h8
zd!vfghew%#xG$feE<>uyLYsJup(zQi)vYssek*`!+i?Ry<#C}t`$86`H(Um>9544b
zeGq_6P_G*zgmbsbmB8D1$EGzRM@&zrL6mb3=Ze-toHC2A@ois2?$Sc^-TCw(0qVP&
zGusms?$L;k!y|L&$+DI&tliRwXdzDJa{_Uo9YRnXMSCgqw<lhxk94mWP*>{M^gL565n=qNiP|v)Y}%
zH_xi|imao-lmyZpJlW*@9s7!8PgVZp{G%eoo;qU8k|1`(5_yxo_+axKiy8$5=eAv(
zisAYB<&tpxMmejKmOmt6=pue`wc1}aVZNmDWX;-f%jNumT)0Z*Y>55whCg1Sg=hkO
zKAl=@eQ);SY5SIUEH7R}!9&*AT^q{e_pQ4jP~)xaVyahV3AF9!mJqFx@cO+1hVUCk
z+Ho&GxW18FRu29&?9g`&9`eYXV9mk|q4^yjBAWy+5wTvaE;#(i7v`mnopD9@5*?h0
z+D5aQ13opzo_WoTVm;T?3D7|@w>B`Uv=%p=sr6tRQjpi>Yn#wMye!#s848`wgyQ4D
zuhLP1V;ZSY!SC6#wF;#a&)&|uQjU9xcyajqgq`{+a8&E(kBgVW03JWQm4;Rf-!FKc
zS{@F>OVR|6uIRbl%@ZDym_Q?mfy_|#+@D4c8V95xmd4z9oyFFh#q*|z@{5;in>fm5
z)33QPFbGxtpTmK4fkDxSqU9V!Ocu@U;q75?L+i_SLuf(c(Z&ht?*@jCx&kGWYMZ*2
zDY^|K^XQ?(ng|{j#%|k6I%fObsI(Q0R`gNS(~M(rnFDM4dQ~#TZg@qCrL1;;8$K$D
zdo^72u*d|}XG6&e$Jn=tM%({1rL{-QQK#ol-3eFFo%_V&+5CN2;QGuk(sON5)f5hU
zYlE81Teu_?a&?{WSuzU(Q)#}ZtZFKs7XA)q!IB!p2kn>Lz*n&E%&H>%h-=XE3y9Pf
z7{21it)cZY$h1aW)}EFZV$Rx=hvZdAhR%lt?j`%l1$%*&(bML>P*YWw;+xz1C_zyVV^N>vVI;LzT
z9}V0mc364kJ=*iCU^JK5;lO^GY|*K+Dvx#G2J*2-9_zAlpC&f@8
z=!dYkK2>38i7Aio4VteH2vf81GLa^ygSQ6#W+%K-#S4jMno1Q6%^5J~94$n5P1x!m
z84+dtTGNMUsI-&2!?(s)UKgKyWjCsEf^@$->#CK5%JDD;g#na$W~t+#qwMygMq0`4wGjjF1PiVkeXO;%6d)ZTHVhu$Wsn`R7&
zNL6sK&3Gkg5Ruvw7f{#N?PNZpmrpA@tE)yxuoEy4xE))FLyJ=K4$eRx-`0vjK^?Np
zU~Y@_YSXgPNdeb)$4+?TIE>)K{I9NhL~7)9Z&9U-;lo2O7D_`!wW|M=_MA&!b^0#i&>2n%R8SLy^`E$V1a(mxfwPJ17>=;imZd}n}sb$GtRS=8`vGvSl@JntI%&Xh;WB6aM2F2@l=x9vHao;t4?F{==t%-xhpZ^cl3kt*6
zJ5rD5*qE7=S|7$Kl?Xh31OxJB>eV8ATMrQ{L4QRcOk4=zfcT=nCXra=747qtpmv3_
z_Cv5pF^r6$$Vj!Y3*a|So{a&s6ItAoRY8%B*OlE`nt9y4Jbbt(@1ojw$Xwj}aj&Ed
zqDbQV`48lBhYks50|WPujuDfiGNkn|9S@HT#i-$yx3xem@C1k
ztIOLUewExOi|LUf8iJX>-%IZ@J3nzyPTh!Kxs$412bQsS`okEG01}_rd#l<4MV-`D
z`LXex3WR8O^AjM^aQ(+Yv@XQC{~_G=I=iXEUlRB(^OeB2TzFn+3vc_DwxpzBd;*j!
zRT=>McGz{xS}daba6qhAuo&OeXLAFR;e$f3O0IXP8?_{Cwvt0x)+5MU=K}BNCQ09O
zV~2|Z^*rS;^0GIB4(N3b7VuwlSNkm<*g5SPh!=?_L@Zufe~O;wUs8%X-@yOH#e;J(
zZ=i1;0qm;M4mj6CZjD3cm?ZUXjS<588db3xS9`-VgKjWewrNHItkJJ>NBm)ZSi0Tu
zOSibANpFtBy!LG!ldG7m_q;oqVP)Ub+6)u;FLEfZzGridx8L9=EuA)VGe=CUbHRSyUA*g&HGbgZ(N$#X{Xg(2+
zQ*Zb}fjw-iKu@>+DZ9fe*j_^&(`3#}9((pE7L)AlDKSyT%9F1~jDB*-c>a^NCIsZ&
zeolENMGJp~OcT5L!K}g{K4U;JR5`a#VDU;HM`1}{S2aBtdTDTpmi&NSnrlSeY2=aj
zuWc)b^gqmbq$K8(Ll|88vE6#BBLy6wWh(=1C{i4+Z)huhc($;#aS>sPIx@!@Re)73
z%E)4mU&dYPX{@52=MBp0ciLNTXa1s1&p+nX&Lb^+D3YtdR%H9>1DVcygo&3>?Q*97
zyyzD6j-!3`kz3?z9DMI1N2nlB=bmgj_+FQoc7dNxfc{d$bV>=Xz7%2UGV7VseDpIV
zEp-rzJxqVH^J_(aq+P@pvTxks?saRRd58RAZRW=V?s`SoDj!CXlfQ~|jIKSYcuTs|
z!khiQp5Ji3sP)al?$G6Ny!Z9#{lJJ{mxyd8#b>g(1Ns(YUJ0eZ*?%1ZjINnpt(N6{ScMr6UAH
zq$eQK1nEs$=n#;iB$OMHOnC2&Z`PWbwPvzb*1Gxbx#!$`XMcO|??l}(&^dhQc_Mlwa&9t@5r002D&DAkU*EAYkE9a9(_qBD&
z-F{H9=%f7_qmW+Va7o~6ow|?QlpDu~L7hCUOlCNxG||ysl;*X$d~Sa%`W$YN()qC6
z)Y2uv$GD_uc8AbZw%SBnx{V^d9~kj8{UU{YtrlWC{M~*Ykp0^rfKp
zTM!*cU%I^@=f$VR8IEu0iP$ufvQh61WfjVl@Tm(hE#vADSq~zU?bVdzs4$~J4FCjw
z>R!__2}oT@BgT#0coTUM#}Tm)Dl51>ImxZ}LcUvM5>^)_c3?u!pWPY4FW`0|^Hjt!
zH{WhR^1#GbgF#O_+zcEU<|BUb2eMRvkAqL))fTm7)egH`ebcP8u=ULHBhk!@*HuU|
zD$Dojb^}c-D{lLMJExc`C@28Tv#~=%>>2>bTMLCNfYb?2F5v0DKOqLXBXOd(YOHp{
zz?4og*4S-$Jo=31g|-Nq(vzVL{{~#IJm*{W;$|f=ZYy^mE2sm{#*{$5wc!jtoPG(O
zdK;$+F6Isr4?a5HnRW+ztMXjrh`o_Dlr>Oxq0^>cEb1V*)1|W=u;iT<2#kqJU1Up
ziD#(M8*Gqid6`fb)Zl2Yr#onevAg6UcXYx3feCuh9cN(=E(|87FjtEUR`Y#^_`L)+
z^DLQENxE|%+$qGU+bT+V&S2S)L
z{PlpjQ!nV#Cn)IMtYQO{jT+b9?^24SHyv%lqLGwDZO*LmJS#QgV^HHukv+l6A;P
zB*n5;9lvY{ZV8;##dfENPQUBa74bE>=vS$!xle4NCjZq%$H5BI+X4y3h}`oR2-4K-
zH&cb1i|ap!nCS~3DPdAOnv)F+PdLa3fAGLzg&ar8oEE89K7A{?|WxFs1*6wWD$nFg*B0wC0b4
z-8w9s8ZX8vT>(Or>!Z7YJF@9www5)VNW-K`u^_DdzH$?Wu9P^_=b
zwMaUL(3LMzC-pM6{!IqwqKGRlRn>g{3Yh<$s7ifr`rZiC
zc+x5#FRq_8i8FNtwd{w&s>@zao?&dV@b7E!`!|C=t{K(--CCV4$}an>Erg+{!*5R%
zt&u&nHmKW~d@-0DxT~vi#CQ=-`&n}0Q+ra_y||RbzC{kOTBA7!f%RAv1W`Br@PDHf
ziGigXZs%I~@m2u3rm$KsOGKHo`ONOGpqkdKS}rlr+slE*pr;k@CYIz=^lmZlG*^lNreYJ=j%Ii9BaIrpB*E~&V@WlBO_EnV=G
zfRor3RpfFPk0tSr{way^6eqo{qEP^kCCz61>G^0gk;h@u=g?77%>hdPF?O6OGH
z5Q&he-HGhHs}05}HEV8Qu|dH;)o3_V*?PVNB)p81Fr8l~b9^3=b~64}M2e_Uv8Ph=
z!Za$}Ry`hd54J?6DIF?;^Ei^Y-YRmQMCKbt%F%m!u}>Ny)@)_{$qHVp-Js@%*9Eti
z@*8B_y)KjF^Ja*O#@L=7<(Og_k7MpGXY45;WzP8bEBTD75*|=1O;kdYzq<2pJ5U)d
zQS!X^Z}(2IBNxeMLmm6ZR7)x4?DM@8W$IXAFJCcNKWuvBm4nr(K2tWYDR7%-c%!N}
z;RbH5U;Z-P=-Z-91Q{(o!xbnu%KuUiq2KENb+{xI6gwkQDAMQN}*@=C4P{i%haUHkJ7wb8q&=73s_mV@PHnl
z!HEM??NOAN&kP}h0l3mwYyf2i9+6L#clYpke1u|cLU!w4d$%bRB(s=-Cp_0UvS^4W
znU1zmL(;CoE1?!6@eq0dJ=N<9Hjthl?QXC~3OV7D?BFlZxCugkL>>An#SR55*gwG7
z{{MylhcS9JMBeJxobJ0j`nowM)gQJefjUHJ?f!sE`F26`Q>n*1-m&CvV=3|L3rM7X
zb`kc8bdJp)1?Be0#|(z&1@;`jyszS1#^a`|x*Ng$j;0uIJarepI
z2;baaKYQH&U1z#*vb-W)qKEG-A@3@$1w4(iDQ$t?$y{zC3{;J?O~w_>+~+f1j>q(p
z{C=M7L&(HHs+(}=)-R(TLGDioYms(cQzvMf=K%Mamsd&65iFrpBncJ5{Xo5JXLeuf
z)g#iI9INj%W~Ie55KKDPyj6gjh^srKPmx{j|-6|-aEzpSa{;ZeNe>Cv8fgD&DC17d&7?1xeAhz;3rWam5
zUzLS;AJkG4+jGNr6^wy2Qyw-^k|OM=9-F3z5vSL!S$Hhg+nzodlEiTNSnhx
z18c#@izWJhI*~-HH!8P2^8wjsuq$HrfhfsC}GuIKzqWj4qt#R~?#n+Z4!>I1=P
zqEJrtbJSayan4XKDUf%#cP`af(~l)S^hXr8dxL5QA5oh1RU>?RksazeS3$`(mKJe^
zyFb3yuBOW}Tn2V+GvbO@06y^?N0jc2c+e;S0c2fIO6tP6fUu;XRJ$YSTOj~f$Jh*y
z@$yzI3(Sjlt6oo~Dbf(OdZ~Hg+$8M`mH+PE{usIlTLs`bk1C49ozVUjfQZ6cJdpR?
zDGpgxjvs~J$FU}93$I0`1V$$DfGoVHhNlbNAQmp4kuK?T%1)P>;z6rx0iv)Gi&^3a
zf!}%R9lc6(-pL=&Ek~-k!8XYDA%Mc0Z280})2QO-R8`_BmGF{Q%72OPR-BKgE!|CQZm
zkKni?LoK=W>0OuIl+|T(_pq9sjg1jc=G0=91?;3{(F60->w>Jsval}SeRP>GVXW6q
zFG>-|Z6hO2NecX-KmS|k1U+$j
z7Be`Z{SwMRr9N|x4ZVCOJeXCBDaVIYjf-R2dDo=*>_2nDoKzz`;-ikFL7b
zy{m-}6DwIaSwxdXGD8pDIo=7%+#|Q|V=Y3=p^!Lt>#nh0?^#5&i|LEO69J07IZ@@~HB@U?fe)MSP@hG9Ljt3NoBJi|0VDj?k`uEhYm4<**7oGeeQh99naB00PA5~bbqNxXMiw9Jp`>5=z4`AI0$;ZuA
zrmr6m!Y(u?)3=1MhMD+6Lb0xc4EkovbV0qz){$YasAyy8lhe+}0}2Ybgt!j4tABeP+;Rj;#7`LS%c(v9~O+W{c=`3K(NBL{CQU+EVca2aoJc6A0k
zWuE1;u>)C=q5r3DK|w?m=xCmsoBII1`d?b;KP3|36#|qTW*6gfj>HOzS2hT9Ry*Q$HL!
z0Qe|g;15$#cJ;*Dn50N%xU&lje*t5+v{)L`$xB{YnIvzxwXNC4HBf_x_JR*iXF>B~
zJ```7X>9bh=sMrg%-DUWZ%E}rX-OUgQdM1QN%2E9T}6=3&kH=iH*OBsi{-Jg*Z)1E
zesO69$8f>3813lZL?@!~$HY&t1kCgEq8FB+81iOV7#YgVLTcFi(C#i26V0A^wQEo7
zI#0Sjye&W?pehG$@HvOEQabsw)YO)wXn*D^QgAQTN%s|NMj(c7j&J}-ShQlboYSAP
zia&||_wfZQ^gn$p1mDjOxSTjkaK|?!d#B4$YIZiSm+wr%3zu-j@q&5;jiqhl9(#iO~q`
z>Uq$E`RPahnMozWk2r=x!+C9->;>)`acj=(Wxe^6-z?Nm=X8cIb`>HVS2LrFVnoYf
zMJDh?(K%>?~iR@z~pxq@y&2jfy%~aaJ
zW^=rP;`A73oQrUjal{z?=GpM@y-!<0hH>ZU$w*W0kCJ}`s2yv~mp@rOZkkt7VH;1@
zu=Wx9^nkT^j_|k10^j}-n->H=YjXxZM$HIY2@UWY0FA%euRu
z@zrK9%J_2*oCyt#O-$FVC<>A?ChL`Vp$?})7#s4q5<##m7i+b$7I?5td3ymXq^}l1
z5coay+y%ehpE{J>i(;yeF^YtHuYm}L7pmHzkJgvj|N1Jicyp7hP-Kwfg8+!9!#vUt
zb>Y!9hBNBZsfsPJYPov3b&&DHrGBA(&*=L#CS(Un?K^i?2~dZ_R~QNFB8&tEm|+SA
zbr`P3i`Zlh4IQQo3LO4IxwI+bnyY*4e$C{?yesam5TaG`UHhVcE^8Xt7)jqNZRp(HNVz|K(L8c{b1g
zs3n42c)U2)u!wa1EW%+>QSaZZhm6{?&3h)GsdR6
zL+*R%;HQTBQlXy|Bt#vn#hRbr!up`m{rrAT1+913*O7fIx9W|_9Py-}%Okf1Co)u-
zh-(QU2lPUC|GJ!_csVE2f-XsWN5ubS(ji~L=qFcPenyIFNRA9}#C(*eBF9eC5kg7<
z_SLlQ?ZD`=#b>YrQbTga&-NnuQI+x4Vb+P`9b$op!+!nL!NSwAPoM|l5j@-E~>BkV*dR#Rj
zpN;_(6pOc1udke4<#_PTIXRwK4hD8gmqOk;{d{@M-H$N8_LrVfIQ|gt#}T6?iC+Lv
zP;&o4`CpN5kV(4ax$XXG@d+D+AX%UuHZ0*t3kq+ySoEE2X$im1-*
z=sE8)LCyP{Z?y8ELssPIYLII-e~)dl6w`kktNabR?BY>=9*%x;Hu&<+b^e^xwQsw1
zE#pI4b@kvEH8FwE<|tAw)XQw!U8ak#V2~uFdRX>oM*curBHUkONM=20=3@_vnU6NK
zS<|dw^i8+r(qeFzx5_n{nwLHtyB6ckn|VWaG9Zc9Dy;1NqSe&uF#UVO1;p9<`Kz=O
z&ote!AI>*Q=k6@kFe#eSPCEs4>a~8m^>h)sOlnNXtAo~5-p-WgOCRcBw%5aXZbick
zZg%1g+o{Mgab94xc{ijbT(gF+WtktL7LPmx+SN7eu&v)NwghE8c28Z9M
z5YUcMZX3@Y9ha+xIu(A=OyYz9O31=51p8+FNQKV4>Jf;dCeW&Ht})q~lRo#Q;9*2#
z!NcqErSBTIEs)Ud?EZ*>n@_Va!0{!5S5JA@rPf2B?^$HNU-#fm#r;INQ0#+;oCBpF17;`M3@_i9$xyB2K3gKi*0^=K#%{D&cMkGyb5akuei%Ew{
z-6A*7aRVXAYVPb8&J5#Bf>iN^>;9J`n}B-$skMEd^^W*ce_Dcp(TZ?D?R@nCZB)#H
z@tJMPvvKAUeJMbc(p?1r56(PP;d6aRQz7J(ef_-i>%>7oHe?+Y>Sj88AYKfp594z*
zv0e!|9UaFq>k7OxIbY39C#p|&IOG8(SEePeQC~4EJzgNgffBCf+fg3%y8tpO8qc=Y
zz`DqfM(`$Sbpj=x%>u{)xakR?zUsWYY2{^o7XZ+k&te(g`jWOsDL7(w4h+42nvYQASe(KL8^ifq)6|*gdk0&Du^g8AV`-cy+uTd
zsDu&-(n1lCP(llV)I0clzuTUB?_VF!b0(88bIzGFXRp22+WUp!J)L8m0-PWa=onP@
zmJtZVlnb;7jzhpND;f2kAP~#R5R(Ufw;X)!e4M@foIO22prEYmxBA8tvOKZqZf^*9c5q=yrn95{N*JHWmHuZ@arm08XY5NcMH}1VmiHXLQ|)hDa#{HT?vIW7
z$QPG`kZcpX{gHsQjxMbjbNSU|XY1KM(-V75SJ=HUJH9*E6+^0ZHQXf?)}xLd;jIqk=K|MzGGgdKOGsMaxc(o4v2D=
zo!^MLQ1@b2nH>n}6mg86O@HhuaZ%Cmymt7-GijeNMUQtv+d}
zE7uZxey^3?XXp&`m-^L+-+A@rubf!%)$D$!l12u&Hr_mLdA1tm?qZe3<@K{dVqdm9
z?$Gn~T!oY3p~pR9fcv>)){hjm^mig(zR`;G{cpF3(t
z4q;)r{SD}yYePvhlA@#)vxJd*WO0(zIe@9{Q4>B*pOQY
z6NkZ<`F&NXYx6DNj-rV6^u+uhF4Z$O+{ke6ss$+sK9>;?*Z(X#axjCxfp-J1{jE-q
zqGX$JTgo=)nfKy+@72Zp=T~is!Z_ip_Z*M9T0hU??Uq%=(YYLm<
zCw+C2f{cxqO8DHB{l)0iYMzImkI6o^;QqW%JHyN(7Z{Fs6JX)=4qC^F{a3SbaOCIx!y
zrb*EF+LX7S$wD?`6K(aFUn`Fq7u}@+ieVAzI%fD>^VRV+k3dT!+hrGSeN2F4y=&ac
z5bf-g=y}U=_sePBIgxb=;sJGh0;jw)D^9(46V+vwaytAZMI(g?3eu|Ty-aRFK~h57
zcigp2J({d@ZXL6&YHpv)
z`#$_Dt#PH#Td3(UV_{DZ-U`k^B9}74?83JA9r7BA>-~&x>-{)CxJ;OU;;r-0V@dh%4&k-)
z2SpgJA&j;SXIIxxMz|wjPwlxNThRFcuJlr^JKcm^Tb+4|SqyU++*C|Efn#m|Xn#{f
zLjxr{Ig#7}bVsw^DcfArM(clI~f`;;&xeD+B7^oJQ=-THuHB~
zTiHLGJaSq7xrAzL97~&STbfS{i;IiX96Z$EFB7GowT)J&J*sGmR}}aA66iUF_oA+i
zICoNq2!faqc+jRUu5T+LjMJX1k-{O+~{P=s!82XiXA4_t3L%=_VZ<
z92%FjpLWTe^N5;1of*=}-eND7yR@{_RPI9=rYQ#y9Dsue{#qn*h+%Mx#Qv%()`hdvb6
z>W6^J#6*?z;z`MhZ&Z@qC<$>edhJ0wMen&G{YzG#5oEXTplJGepWW-~6{q80o~_;8
zYemN-Mu@y^dbqs2oKkSruKN6Gbl>3VBKDiuc@D>
zlCUC(`HyXd?f%?gytyAij_z$iop18kSTX^6f4L==`@L>E*bfb!mh19k6wuAA5v4H(
zr~4rh-&G^7VEdN@1@V)yj^D+hDoC08yB7&>#D#NGm%m1D
zEdPRq831z=kb9C4pbmRqUw?gSpT-HDcRt%jrLz2CWAal9RP|kCd=J&upnZBWtjr+-
zf}SMHh)IXem@SPf1&{BCy?`(`2i(i)6KPE2;sbHy{tjt7U$|;8{n1Ij^xy+)X)AOR
zBpUG}A(HI)b9B}7btz)hO&bET5sOIK=aV3iS|~c_JQ%uPiS&@1M4l~rOw-LVWd?{t
zRPw-rCnYNedaTS{D;hQHH90^w_mHF(qc#)COm(N8oh@)NQ-0#BJ^fP>s(A
zi?hxi$cpsMvUKYq94^e)yr`?G`uY|Vh&QXR@6L*ht)=)}MbjK-T|2Elmb9F2Ku0lp
zaPI6N=H5pf3OB6YY3%_~h9A^EF(lvB<)WET1Dw$GK|AtEO+aJH4#j-8%N-I*W5ydp
zmX%dhXeB?BlDH6|Po7+$7|^T{baszE5((kEa|c2B$z9=}LT6vD(QcdN5=fJT`dFFk
zBnjQu*4D;;D^AH=<;f@)UjN*yl~N#pHT_t?HYN5Eud{H0BF?%J2mn=}lpBVz+kpD$
z8NQv_y#fjJGr@94>2#?nlGEz6NsK>uO9i)CLc%)2(Xl8qHFbF2_rr$|9yKa?GxUq~
zHw7s$3yV@JHO!9UN!cqJ+OO>GAFaq%F?Fh2%(5VC@oRg-TjDJgjy
z%O)l!cJJasWe!c`F4GK-vhh-I#v7QC5$A6y=e1WtXXOy|u*5TR-G2@q)$Dday|}Xr
zY5rW)8Y7HIs?WN@E*VeBz)~7FcNW!~
zH3!DYcli_X(k$|WHtc(CyYtQNy{21>w^lMO5=W4Vo;jnHuu9?zmO_fWJ8F;>5IP&f
zpD+r`k-L{_F1!PlnerQOf)msumL(C8FxxKRZLPV4#!evl)!Dg(41