diff --git a/baystation12.dme b/baystation12.dme
index 4746152b2a..1b78413fc6 100644
--- a/baystation12.dme
+++ b/baystation12.dme
@@ -1121,6 +1121,7 @@
#include "code\modules\mob\living\silicon\ai\ai.dm"
#include "code\modules\mob\living\silicon\ai\death.dm"
#include "code\modules\mob\living\silicon\ai\examine.dm"
+#include "code\modules\mob\living\silicon\ai\latejoin.dm"
#include "code\modules\mob\living\silicon\ai\laws.dm"
#include "code\modules\mob\living\silicon\ai\life.dm"
#include "code\modules\mob\living\silicon\ai\login.dm"
diff --git a/code/game/jobs/job/job.dm b/code/game/jobs/job/job.dm
index 0c44f6f260..c0d9bd44b3 100644
--- a/code/game/jobs/job/job.dm
+++ b/code/game/jobs/job/job.dm
@@ -110,3 +110,6 @@
if(H.r_store)
H.r_store.add_fingerprint(H,1)
return 1
+
+/datum/job/proc/is_position_available()
+ return (current_positions < total_positions) || (total_positions == -1)
diff --git a/code/game/jobs/job/silicon.dm b/code/game/jobs/job/silicon.dm
index 07ceefb8b3..11d8f70a12 100644
--- a/code/game/jobs/job/silicon.dm
+++ b/code/game/jobs/job/silicon.dm
@@ -3,7 +3,7 @@
flag = AI
department_flag = ENGSEC
faction = "Station"
- total_positions = 0
+ total_positions = 0 // Not used for AI, see is_position_available below and modules/mob/living/silicon/ai/latejoin.dm
spawn_positions = 1
selection_color = "#ccffcc"
supervisors = "your laws"
@@ -14,6 +14,8 @@
if(!H) return 0
return 1
+/datum/job/ai/is_position_available()
+ return (empty_playable_ai_cores.len != 0)
/datum/job/cyborg
diff --git a/code/game/jobs/job_controller.dm b/code/game/jobs/job_controller.dm
index b3918713e7..87f8989b10 100644
--- a/code/game/jobs/job_controller.dm
+++ b/code/game/jobs/job_controller.dm
@@ -463,7 +463,9 @@ var/global/datum/controller/occupations/job_master
switch(rank)
if("Cyborg")
return H.Robotize()
- if("AI","Clown") //don't need bag preference stuff!
+ if("AI")
+ return H
+ if("Clown") //don't need bag preference stuff!
else
switch(H.backbag) //BS12 EDIT
if(1)
diff --git a/code/game/machinery/computer/ai_core.dm b/code/game/machinery/computer/ai_core.dm
index 69c884ec70..7bd8e4b8e8 100644
--- a/code/game/machinery/computer/ai_core.dm
+++ b/code/game/machinery/computer/ai_core.dm
@@ -168,9 +168,15 @@
if(istype(P, /obj/item/weapon/screwdriver))
playsound(loc, 'sound/items/Screwdriver.ogg', 50, 1)
user << "\blue You connect the monitor."
- var/mob/living/silicon/ai/A = new /mob/living/silicon/ai ( loc, laws, brain )
- if(A) //if there's no brain, the mob is deleted and a structure/AIcore is created
- A.rename_self("ai", 1)
+ if(!brain)
+ var/open_for_latejoin = alert(user, "Would you like this core to be open for latejoining AIs?", "Latejoin", "Yes", "Yes", "No") == "Yes"
+ var/obj/structure/AIcore/deactivated/D = new(loc)
+ if(open_for_latejoin)
+ empty_playable_ai_cores += D
+ else
+ var/mob/living/silicon/ai/A = new /mob/living/silicon/ai ( loc, laws, brain )
+ if(A) //if there's no brain, the mob is deleted and a structure/AIcore is created
+ A.rename_self("ai", 1)
feedback_inc("cyborg_ais_created",1)
del(src)
@@ -207,14 +213,53 @@
if (ai.mind == malfai)
return 1
-/obj/structure/AIcore/deactivated/attackby(var/obj/item/device/aicard/card, var/mob/user)
+/obj/structure/AIcore/deactivated/attackby(var/obj/item/weapon/W, var/mob/user)
- if(istype(card))
+ if(istype(W, /obj/item/device/aicard))
+ var/obj/item/device/aicard/card = W
var/mob/living/silicon/ai/transfer = locate() in card
if(transfer)
load_ai(transfer,card,user)
else
user << "\red ERROR: \black Unable to locate artificial intelligence."
return
+ else if(istype(W, /obj/item/weapon/wrench))
+ if(anchored)
+ user.visible_message("\blue \The [user] starts to unbolt \the [src] from the plating...")
+ if(!do_after(user,40))
+ user.visible_message("\blue \The [user] decides not to unbolt \the [src].")
+ return
+ user.visible_message("\blue \The [user] finishes unfastening \the [src]!")
+ anchored = 0
+ return
+ else
+ user.visible_message("\blue \The [user] starts to bolt \the [src] to the plating...")
+ if(!do_after(user,40))
+ user.visible_message("\blue \The [user] decides not to bolt \the [src].")
+ return
+ user.visible_message("\blue \The [user] finishes fastening down \the [src]!")
+ anchored = 1
+ return
+ else
+ return ..()
- ..()
+/client/proc/empty_ai_core_toggle_latejoin()
+ set name = "Toggle AI Core Latejoin"
+ set category = "Admin"
+
+ var/list/cores = list()
+ for(var/obj/structure/AIcore/deactivated/D in world)
+ cores["[D] ([D.loc.loc])"] = D
+
+ var/id = input("Which core?", "Toggle AI Core Latejoin", null) as null|anything in cores
+ if(!id) return
+
+ var/obj/structure/AIcore/deactivated/D = cores[id]
+ if(!D) return
+
+ if(D in empty_playable_ai_cores)
+ empty_playable_ai_cores -= D
+ src << "\The [id] is now not available for latejoining AIs."
+ else
+ empty_playable_ai_cores += D
+ src << "\The [id] is now available for latejoining AIs."
diff --git a/code/global.dm b/code/global.dm
index 19ddbbe289..13f10c0764 100644
--- a/code/global.dm
+++ b/code/global.dm
@@ -231,3 +231,6 @@ var/static/list/scarySounds = list('sound/weapons/thudswoosh.ogg','sound/weapons
// Bomb cap!
var/max_explosion_range = 14
+
+// Announcer intercom, because too much stuff creates an intercom for one message then hard del()s it.
+var/global/obj/item/device/radio/intercom/global_announcer = new(null)
diff --git a/code/modules/admin/admin_verbs.dm b/code/modules/admin/admin_verbs.dm
index d20105a67e..823fb69f80 100644
--- a/code/modules/admin/admin_verbs.dm
+++ b/code/modules/admin/admin_verbs.dm
@@ -78,7 +78,8 @@ var/list/admin_verbs_admin = list(
/client/proc/toggle_antagHUD_use,
/client/proc/toggle_antagHUD_restrictions,
/client/proc/allow_character_respawn, /* Allows a ghost to respawn */
- /client/proc/event_manager_panel
+ /client/proc/event_manager_panel,
+ /client/proc/empty_ai_core_toggle_latejoin
)
var/list/admin_verbs_ban = list(
/client/proc/unban_panel,
diff --git a/code/modules/mob/living/silicon/ai/ai.dm b/code/modules/mob/living/silicon/ai/ai.dm
index ff948d2387..c25e833c08 100644
--- a/code/modules/mob/living/silicon/ai/ai.dm
+++ b/code/modules/mob/living/silicon/ai/ai.dm
@@ -141,7 +141,7 @@ var/list/ai_verbs_default = list(
if(!safety)//Only used by AIize() to successfully spawn an AI.
if (!B)//If there is no player/brain inside.
- new/obj/structure/AIcore/deactivated(loc)//New empty terminal.
+ empty_playable_ai_cores += new/obj/structure/AIcore/deactivated(loc)//New empty terminal.
del(src)//Delete AI.
return
else
diff --git a/code/modules/mob/living/silicon/ai/latejoin.dm b/code/modules/mob/living/silicon/ai/latejoin.dm
new file mode 100644
index 0000000000..8aca00a711
--- /dev/null
+++ b/code/modules/mob/living/silicon/ai/latejoin.dm
@@ -0,0 +1,44 @@
+var/global/list/empty_playable_ai_cores = list()
+
+/hook/roundstart/proc/spawn_empty_ai()
+ for(var/obj/effect/landmark/start/S in landmarks_list)
+ if(S.name != "AI")
+ continue
+ if(locate(/mob/living) in S.loc)
+ continue
+ empty_playable_ai_cores += new /obj/structure/AIcore/deactivated(get_turf(S))
+
+ return 1
+
+/mob/living/silicon/ai/verb/wipe_core()
+ set name = "Wipe Core"
+ set category = "OOC"
+ set desc = "Wipe your core. This is functionally equivalent to cryo or robotic storage, freeing up your job slot."
+
+ if(ticker && ticker.mode && ticker.mode.name == "AI malfunction")
+ usr << "You cannot use this verb in malfunction. If you need to leave, please adminhelp."
+ return
+
+ // Guard against misclicks, this isn't the sort of thing we want happening accidentally
+ if(alert("WARNING: This will immediately wipe your core and ghost you, removing your character from the round permanently (similar to cryo and robotic storage). Are you entirely sure you want to do this?",
+ "Wipe Core", "No", "No", "Yes") != "Yes")
+ return
+
+ // We warned you.
+ empty_playable_ai_cores += new /obj/structure/AIcore/deactivated(loc)
+ global_announcer.autosay("[src] has been moved to intelligence storage.", "Artificial Intelligence Oversight")
+
+ //Handle job slot/tater cleanup.
+ var/job = mind.assigned_role
+
+ job_master.FreeRole(job)
+
+ if(mind.objectives.len)
+ del(mind.objectives)
+ mind.special_role = null
+ else
+ if(ticker.mode.name == "AutoTraitor")
+ var/datum/game_mode/traitor/autotraitor/current_mode = ticker.mode
+ current_mode.possible_traitors.Remove(src)
+
+ del(src)
\ No newline at end of file
diff --git a/code/modules/mob/new_player/new_player.dm b/code/modules/mob/new_player/new_player.dm
index a07e538e1e..78817c9ccf 100644
--- a/code/modules/mob/new_player/new_player.dm
+++ b/code/modules/mob/new_player/new_player.dm
@@ -284,7 +284,7 @@
proc/IsJobAvailable(rank)
var/datum/job/job = job_master.GetJob(rank)
if(!job) return 0
- if((job.current_positions >= job.total_positions) && job.total_positions != -1) return 0
+ if(!job.is_position_available()) return 0
if(jobban_isbanned(src,rank)) return 0
if(!job.player_old_enough(src.client)) return 0
return 1
@@ -313,6 +313,24 @@
UpdateFactionList(character)
EquipCustomItems(character)
+ // AIs don't need a spawnpoint, they must spawn at an empty core
+ if(character.mind.assigned_role == "AI")
+
+ character = character.AIize(move=0) // AIize the character, but don't move them yet
+
+ // IsJobAvailable for AI checks that there is an empty core available in this list
+ var/obj/structure/AIcore/deactivated/C = empty_playable_ai_cores[1]
+ empty_playable_ai_cores -= C
+
+ character.loc = C.loc
+
+ AnnounceCyborg(character, rank, "has been downloaded to the empty core in \the [character.loc.loc]")
+ ticker.mode.latespawn(character)
+
+ del(C)
+ del(src)
+ return
+
//Find our spawning point.
var/join_message
var/datum/spawnpoint/S
@@ -356,20 +374,16 @@
proc/AnnounceArrival(var/mob/living/carbon/human/character, var/rank, var/join_message)
if (ticker.current_state == GAME_STATE_PLAYING)
- var/obj/item/device/radio/intercom/a = new /obj/item/device/radio/intercom(null)// BS12 EDIT Arrivals Announcement Computer, rather than the AI.
if(character.mind.role_alt_title)
rank = character.mind.role_alt_title
- a.autosay("[character.real_name],[rank ? " [rank]," : " visitor," ] [join_message ? join_message : "has arrived on the station"].", "Arrivals Announcement Computer")
- del(a)
+ global_announcer.autosay("[character.real_name],[rank ? " [rank]," : " visitor," ] [join_message ? join_message : "has arrived on the station"].", "Arrivals Announcement Computer")
proc/AnnounceCyborg(var/mob/living/character, var/rank, var/join_message)
if (ticker.current_state == GAME_STATE_PLAYING)
- var/obj/item/device/radio/intercom/a = new /obj/item/device/radio/intercom(null)// BS12 EDIT Arrivals Announcement Computer, rather than the AI.
if(character.mind.role_alt_title)
rank = character.mind.role_alt_title
// can't use their name here, since cyborg namepicking is done post-spawn, so we'll just say "A new Cyborg has arrived"/"A new Android has arrived"/etc.
- a.autosay("A new[rank ? " [rank]" : " visitor" ] [join_message ? join_message : "has arrived on the station"].", "Arrivals Announcement Computer")
- del(a)
+ global_announcer.autosay("A new[rank ? " [rank]" : " visitor" ] [join_message ? join_message : "has arrived on the station"].", "Arrivals Announcement Computer")
proc/LateChoices()
var/mills = world.time // 1/10 of a second, not real milliseconds but whatever
diff --git a/code/modules/mob/transform_procs.dm b/code/modules/mob/transform_procs.dm
index b4530b0767..d6181f5ec7 100644
--- a/code/modules/mob/transform_procs.dm
+++ b/code/modules/mob/transform_procs.dm
@@ -56,13 +56,13 @@
spawning = 1
return ..()
-/mob/living/carbon/human/AIize()
+/mob/living/carbon/human/AIize(move=1) // 'move' argument needs defining here too because BYOND is dumb
if (monkeyizing)
return
for(var/t in organs)
del(t)
- return ..()
+ return ..(move)
/mob/living/carbon/AIize()
if (monkeyizing)
@@ -75,7 +75,7 @@
invisibility = 101
return ..()
-/mob/proc/AIize()
+/mob/proc/AIize(move=1)
if(client)
src << sound(null, repeat = 0, wait = 0, volume = 85, channel = 1) // stop the jams for AIs
var/mob/living/silicon/ai/O = new (loc, base_law_type,,1)//No MMI but safety is in effect.
@@ -88,37 +88,38 @@
else
O.key = key
- var/obj/loc_landmark
- for(var/obj/effect/landmark/start/sloc in landmarks_list)
- if (sloc.name != "AI")
- continue
- if (locate(/mob/living) in sloc.loc)
- continue
- loc_landmark = sloc
- if (!loc_landmark)
- for(var/obj/effect/landmark/tripai in landmarks_list)
- if (tripai.name == "tripai")
- if(locate(/mob/living) in tripai.loc)
- continue
- loc_landmark = tripai
- if (!loc_landmark)
- O << "Oh god sorry we can't find an unoccupied AI spawn location, so we're spawning you on top of someone."
+ if(move)
+ var/obj/loc_landmark
for(var/obj/effect/landmark/start/sloc in landmarks_list)
- if (sloc.name == "AI")
- loc_landmark = sloc
+ if (sloc.name != "AI")
+ continue
+ if ((locate(/mob/living) in sloc.loc) || (locate(/obj/structure/AIcore) in sloc.loc))
+ continue
+ loc_landmark = sloc
+ if (!loc_landmark)
+ for(var/obj/effect/landmark/tripai in landmarks_list)
+ if (tripai.name == "tripai")
+ if((locate(/mob/living) in tripai.loc) || (locate(/obj/structure/AIcore) in tripai.loc))
+ continue
+ loc_landmark = tripai
+ if (!loc_landmark)
+ O << "Oh god sorry we can't find an unoccupied AI spawn location, so we're spawning you on top of someone."
+ for(var/obj/effect/landmark/start/sloc in landmarks_list)
+ if (sloc.name == "AI")
+ loc_landmark = sloc
- O.loc = loc_landmark.loc
- for (var/obj/item/device/radio/intercom/comm in O.loc)
- comm.ai += O
+ O.loc = loc_landmark.loc
+ for (var/obj/item/device/radio/intercom/comm in O.loc)
+ comm.ai += O
O.on_mob_init()
O.add_ai_verbs()
O.rename_self("ai",1)
- . = O
- del(src)
-
+ spawn(0)
+ del(src)
+ return O
//human -> robot
/mob/living/carbon/human/proc/Robotize()