Voice Announcement System for AIs and Captains (#11548)

* Voice Announcement System

* Fixes shit the linter complained about

* Uses topic instead of JSON files

* fixes alex's problems

* use /dev/urandom on linux

* GenerateToken

* update the config in theory

* Allow downloading sounds via Get Server Logs
This commit is contained in:
Maxim Nikitin
2021-07-01 06:46:27 -04:00
committed by GitHub
parent cce3787ace
commit 7013fa39d5
14 changed files with 276 additions and 17 deletions

View File

@@ -7,12 +7,9 @@
"tgui/.yarn": true, "tgui/.yarn": true,
"tgui/.pnp.*": true "tgui/.pnp.*": true
}, },
"workbench.editorAssociations": [ "workbench.editorAssociations": {
{ "*.dmi": "imagePreview.previewEditor"
"filenamePattern": "*.dmi", },
"viewType": "imagePreview.previewEditor"
}
],
"files.eol": "\n", "files.eol": "\n",
"gitlens.advanced.blame.customArguments": ["-w"] "gitlens.advanced.blame.customArguments": ["-w"]
} }

View File

@@ -8,11 +8,12 @@
#define CHANNEL_AMBIENCE 1018 #define CHANNEL_AMBIENCE 1018
#define CHANNEL_BUZZ 1017 #define CHANNEL_BUZZ 1017
#define CHANNEL_BICYCLE 1016 #define CHANNEL_BICYCLE 1016
#define CHANNEL_VOICE_ANNOUNCE 1015
//THIS SHOULD ALWAYS BE THE LOWEST ONE! //THIS SHOULD ALWAYS BE THE LOWEST ONE!
//KEEP IT UPDATED //KEEP IT UPDATED
#define CHANNEL_HIGHEST_AVAILABLE 1015 #define CHANNEL_HIGHEST_AVAILABLE 1014
#define MAX_INSTRUMENT_CHANNELS (128 * 6) #define MAX_INSTRUMENT_CHANNELS (128 * 6)

View File

@@ -1,4 +1,4 @@
/client/proc/browse_files(root="data/logs/", max_iterations=10, list/valid_extensions=list("txt","log","htm", "html", "json")) /client/proc/browse_files(root="data/logs/", max_iterations=10, list/valid_extensions=list("txt","log","htm", "html", "json", "aac", "mp3", "ogg", "opus", "wav", "weba"))
if(IsAdminAdvancedProcCall()) if(IsAdminAdvancedProcCall())
log_admin_private("BROWSEFILES: Admin proc call blocked") log_admin_private("BROWSEFILES: Admin proc call blocked")
message_admins("BROWSEFILES: Admin proc call blocked") message_admins("BROWSEFILES: Admin proc call blocked")

View File

@@ -274,6 +274,10 @@
/datum/config_entry/string/invoke_youtubedl /datum/config_entry/string/invoke_youtubedl
protection = CONFIG_ENTRY_LOCKED | CONFIG_ENTRY_HIDDEN protection = CONFIG_ENTRY_LOCKED | CONFIG_ENTRY_HIDDEN
/datum/config_entry/string/voice_announce_url_base
/datum/config_entry/string/voice_announce_dir
/datum/config_entry/flag/show_irc_name /datum/config_entry/flag/show_irc_name
/datum/config_entry/flag/see_own_notes //Can players see their own admin notes /datum/config_entry/flag/see_own_notes //Can players see their own admin notes

View File

@@ -7,6 +7,7 @@ SUBSYSTEM_DEF(communications)
var/silicon_message_cooldown var/silicon_message_cooldown
var/nonsilicon_message_cooldown var/nonsilicon_message_cooldown
var/last_voice_announce_open = 0
/datum/controller/subsystem/communications/proc/can_announce(mob/living/user, is_silicon) /datum/controller/subsystem/communications/proc/can_announce(mob/living/user, is_silicon)
if(is_silicon && silicon_message_cooldown > world.time) if(is_silicon && silicon_message_cooldown > world.time)

View File

@@ -0,0 +1,173 @@
GLOBAL_LIST_EMPTY(voice_announce_list)
/datum/voice_announce
var/id
var/client/client
var/is_ai = FALSE
var/started_playing = FALSE
var/duration = 300
var/canceled = FALSE
var/was_queried = FALSE
/datum/voice_announce/New(client/client)
. = ..()
src.client = client
id = "[client.ckey]_[GenerateToken()]"
/datum/voice_announce/Destroy()
GLOB.voice_announce_list -= id
. = ..()
/datum/voice_announce/proc/open()
if(SScommunications.last_voice_announce_open + 30 > world.time)
// Keep cheeky fucks from trying to waste resources by spamming the button
return
var/url_base = CONFIG_GET(string/voice_announce_url_base)
var/dir = CONFIG_GET(string/voice_announce_dir)
if(!url_base || !dir)
return
if(is_banned_from(client.ckey, "Voice Announcements"))
to_chat(client, "<span class='warning>You are banned from making voice announcements.</span>")
return
SScommunications.last_voice_announce_open = world.time
GLOB.voice_announce_list[id] = src
usr << link(url_base + "[CONFIG_GET(string/serversqlname)]/[id]")
addtimer(CALLBACK(src, .proc/timeout1), 15 SECONDS)
addtimer(CALLBACK(src, .proc/timeout2), 5 MINUTES)
/datum/voice_announce/proc/timeout1()
if(!was_queried && !started_playing)
qdel(src)
/datum/voice_announce/proc/timeout2()
if(!started_playing)
qdel(src)
/datum/voice_announce/Topic(href, href_list)
. = ..()
if(href_list["stop_announce"] && started_playing && !canceled && check_rights(NONE))
SEND_SOUND(world, sound(null, channel = CHANNEL_VOICE_ANNOUNCE))
canceled = TRUE
log_admin("[key_name(usr)] has canceled the voice announcement")
message_admins("[key_name_admin(usr)] has canceled the voice announcement")
/datum/voice_announce/proc/do_announce_sound(sound/sound1, sound/sound2, sounds_delay, z_level)
started_playing = TRUE
// Play for admins
for(var/mob/M in GLOB.player_list)
if(M.client && M.client.holder && M.can_hear() && (M.client.prefs.toggles & SOUND_ANNOUNCEMENTS))
var/turf/T = get_turf(M)
if(T.z == z_level)
SEND_SOUND(M, sound1)
sleep(sounds_delay)
for(var/mob/M in GLOB.player_list)
if(M.client && M.client.holder && M.can_hear() && (M.client.prefs.toggles & SOUND_ANNOUNCEMENTS))
var/turf/T = get_turf(M)
if(T.z == z_level)
SEND_SOUND(M, sound2)
sleep(30)
if(canceled)
return
// Play for everyone else
for(var/mob/M in GLOB.player_list)
if(M.client && !M.client.holder && M.can_hear() && (M.client.prefs.toggles & SOUND_ANNOUNCEMENTS))
var/turf/T = get_turf(M)
if(T.z == z_level)
SEND_SOUND(M, sound1)
sleep(sounds_delay)
if(canceled)
return
for(var/mob/M in GLOB.player_list)
if(M.client && !M.client.holder && M.can_hear() && (M.client.prefs.toggles & SOUND_ANNOUNCEMENTS))
var/turf/T = get_turf(M)
if(T.z == z_level)
SEND_SOUND(M, sound2)
sleep(duration)
qdel(src)
/datum/voice_announce/proc/check_valid()
if(client == null)
return FALSE
if(is_banned_from(client.ckey, "Voice Announcements"))
to_chat(client, "<span class='warning>You are banned from making voice announcements.</span>")
return FALSE
return TRUE
/datum/voice_announce/proc/announce(snd)
stack_trace("announce() is unimplemented")
/datum/voice_announce/proc/handle_announce(ogg_filename, base_filename, ip, duration)
src.duration = duration
GLOB.voice_announce_list -= id
var/ogg_file = file("[CONFIG_GET(string/voice_announce_dir)]/[ogg_filename]")
var/base_file = file("[CONFIG_GET(string/voice_announce_dir)]/[base_filename]")
if(check_valid())
var/snd = fcopy_rsc(ogg_file)
fdel(ogg_file)
fcopy(base_file, "[GLOB.log_directory]/[base_filename]")
fdel(base_file)
log_admin("[key_name(client)] has made a voice announcement via [ip], saved to [base_filename]")
message_admins("[key_name_admin(client)] has made a voice announcement. ((<a href='?src=[REF(src)]&stop_announce=1'>CANCEL</a>))")
announce(snd)
else
fdel(ogg_file)
fdel(base_file)
/datum/voice_announce/ai
is_ai = TRUE
/datum/voice_announce/ai/check_valid()
if(!..())
return FALSE
var/mob/living/silicon/ai/M = client.mob
if(!istype(M))
return FALSE
if(GLOB.announcing_vox > world.time || M.control_disabled || M.incapacitated())
return FALSE
return TRUE
/datum/voice_announce/ai/announce(snd)
set waitfor = 0
GLOB.announcing_vox = world.time + 600
var/turf/mob_turf = get_turf(client.mob)
var/z_level = mob_turf.z
var/sound/sound1 = sound('sound/vox/doop.ogg')
var/sound/sound2 = sound(snd, channel = CHANNEL_VOICE_ANNOUNCE, volume = 70)
do_announce_sound(sound1, sound2, 5, z_level)
/datum/voice_announce/command
var/obj/machinery/computer/communications/comms_console
/datum/voice_announce/command/New(client/client, obj/machinery/computer/communications/console)
. = ..()
comms_console = console
/datum/voice_announce/command/check_valid()
if(!..())
return FALSE
var/mob/living/user = client.mob
if(!SScommunications.can_announce(user, issilicon(user)))
return FALSE
if(!istype(user) || !user.canUseTopic(comms_console, !issilicon(user)))
return FALSE
return TRUE
/datum/voice_announce/command/announce(snd)
set waitfor = 0
var/turf/console_turf = get_turf(comms_console)
var/z_level = console_turf.z
SScommunications.nonsilicon_message_cooldown = world.time + 300
var/mob/living/user = client.mob
deadchat_broadcast(" made a priority announcement from <span class='name'>[get_area_name(user, TRUE)]</span>.", "<span class='name'>[user.real_name]</span>", user)
var/sound/sound1 = sound('sound/misc/announce.ogg')
var/sound/sound2 = sound(snd, channel = CHANNEL_VOICE_ANNOUNCE, volume = 70)
do_announce_sound(sound1, sound2, 15, z_level)

View File

@@ -174,6 +174,41 @@
return jointext(message, "") return jointext(message, "")
// Plays a voice announcement, given the ID of a voice annoucnement datum and a filename of a file in the shared folder, among other things
/datum/world_topic/voice_announce
keyword = "voice_announce"
require_comms_key = TRUE
/datum/world_topic/voice_announce/Run(list/input)
var/datum/voice_announce/A = GLOB.voice_announce_list[input["voice_announce"]]
if(istype(A))
A.handle_announce(input["ogg_file"], input["uploaded_file"], input["ip"], text2num(input["duration"]))
// Cancels a voice announcement, given the ID of voice announcement datum, used if the user closes their browser window instead of uploading
/datum/world_topic/voice_announce_cancel
keyword = "voice_announce_cancel"
require_comms_key = TRUE
/datum/world_topic/voice_announce_cancel/Run(list/input)
var/datum/voice_announce/A = GLOB.voice_announce_list[input["voice_announce_cancel"]]
if(istype(A))
qdel(A)
// Queries information about a voice announcement.
/datum/world_topic/voice_announce_query
keyword = "voice_announce_query"
require_comms_key = TRUE
/datum/world_topic/voice_announce_query/Run(list/input)
. = list()
var/datum/voice_announce/A = GLOB.voice_announce_list[input["voice_announce_query"]]
if(istype(A))
A.was_queried = TRUE
.["exists"] = TRUE
.["is_ai"] = A.is_ai
else
.["exists"] = FALSE
/datum/world_topic/status /datum/world_topic/status
keyword = "status" keyword = "status"

View File

@@ -161,6 +161,10 @@
if (!authenticated_as_silicon_or_captain(usr)) if (!authenticated_as_silicon_or_captain(usr))
return return
make_announcement(usr) make_announcement(usr)
if ("makeVoiceAnnouncement")
if (!authenticated_as_non_silicon_captain(usr))
return
make_voice_announcement(usr)
if ("messageAssociates") if ("messageAssociates")
if (!authenticated_as_non_silicon_captain(usr)) if (!authenticated_as_non_silicon_captain(usr))
return return
@@ -343,6 +347,7 @@
if (STATE_MAIN) if (STATE_MAIN)
data["canBuyShuttles"] = can_buy_shuttles(user) data["canBuyShuttles"] = can_buy_shuttles(user)
data["canMakeAnnouncement"] = FALSE data["canMakeAnnouncement"] = FALSE
data["canMakeVoiceAnnouncement"] = FALSE
data["canMessageAssociates"] = FALSE data["canMessageAssociates"] = FALSE
data["canRecallShuttles"] = !issilicon(user) data["canRecallShuttles"] = !issilicon(user)
data["canRequestNuke"] = FALSE data["canRequestNuke"] = FALSE
@@ -381,6 +386,7 @@
data["alertLevelTick"] = alert_level_tick data["alertLevelTick"] = alert_level_tick
data["canMakeAnnouncement"] = TRUE data["canMakeAnnouncement"] = TRUE
data["canMakeVoiceAnnouncement"] = ishuman(user)
data["canSetAlertLevel"] = issilicon(user) ? "NO_SWIPE_NEEDED" : "SWIPE_NEEDED" data["canSetAlertLevel"] = issilicon(user) ? "NO_SWIPE_NEEDED" : "SWIPE_NEEDED"
if (SSshuttle.emergency.mode != SHUTTLE_IDLE && SSshuttle.emergency.mode != SHUTTLE_RECALL) if (SSshuttle.emergency.mode != SHUTTLE_IDLE && SSshuttle.emergency.mode != SHUTTLE_RECALL)
@@ -481,6 +487,14 @@
SScommunications.make_announcement(user, is_ai, input) SScommunications.make_announcement(user, is_ai, input)
deadchat_broadcast(" made a priority announcement from <span class='name'>[get_area_name(usr, TRUE)]</span>.", "<span class='name'>[user.real_name]</span>", user) deadchat_broadcast(" made a priority announcement from <span class='name'>[get_area_name(usr, TRUE)]</span>.", "<span class='name'>[user.real_name]</span>", user)
/obj/machinery/computer/communications/proc/make_voice_announcement(mob/living/user)
if(!SScommunications.can_announce(user, FALSE))
to_chat(user, "<span class='alert'>Intercomms recharging. Please stand by.</span>")
return
var/datum/voice_announce/command/announce_datum = new(user.client, src)
announce_datum.open()
/obj/machinery/computer/communications/proc/post_status(command, data1, data2) /obj/machinery/computer/communications/proc/post_status(command, data1, data2)
var/datum/radio_frequency/frequency = SSradio.return_frequency(FREQ_STATUS_DISPLAYS) var/datum/radio_frequency/frequency = SSradio.return_frequency(FREQ_STATUS_DISPLAYS)

View File

@@ -252,7 +252,7 @@
output += "</div></div>" output += "</div></div>"
//departments/groups that don't have command staff would throw a javascript error since there's no corresponding reference for toggle_head() //departments/groups that don't have command staff would throw a javascript error since there's no corresponding reference for toggle_head()
var/list/headless_job_lists = list("Silicon" = GLOB.nonhuman_positions, var/list/headless_job_lists = list("Silicon" = GLOB.nonhuman_positions,
"Abstract" = list("Appearance", "Emote", "OOC")) "Abstract" = list("Appearance", "Emote", "OOC", "Voice Announcements"))
for(var/department in headless_job_lists) for(var/department in headless_job_lists)
output += "<div class='column'><label class='rolegroup [ckey(department)]'><input type='checkbox' name='[department]' class='hidden' [usr.client.prefs.tgui_fancy ? " onClick='toggle_checkboxes(this, \"_com\")'" : ""]>[department]</label><div class='content'>" output += "<div class='column'><label class='rolegroup [ckey(department)]'><input type='checkbox' name='[department]' class='hidden' [usr.client.prefs.tgui_fancy ? " onClick='toggle_checkboxes(this, \"_com\")'" : ""]>[department]</label><div class='content'>"
break_counter = 0 break_counter = 0

View File

@@ -85,20 +85,40 @@
popup.set_content(dat) popup.set_content(dat)
popup.open() popup.open()
/mob/living/silicon/ai/proc/voice_announce()
if(GLOB.announcing_vox > world.time)
to_chat(src, "<span class='notice'>Please wait [DisplayTimeText(GLOB.announcing_vox - world.time)].</span>")
return
if(incapacitated())
return
if(control_disabled)
to_chat(src, "<span class='warning'>Wireless interface disabled, unable to interact with announcement PA.</span>")
return
var/datum/voice_announce/ai/announce_datum = new(client)
announce_datum.open()
GLOBAL_VAR_INIT(announcing_vox, 0)
/mob/living/silicon/ai/proc/announcement() /mob/living/silicon/ai/proc/announcement()
var/static/announcing_vox = 0 // Stores the time of the last announcement if(GLOB.announcing_vox > world.time)
if(announcing_vox > world.time) to_chat(src, "<span class='notice'>Please wait [DisplayTimeText(GLOB.announcing_vox - world.time)].</span>")
to_chat(src, "<span class='notice'>Please wait [DisplayTimeText(announcing_vox - world.time)].</span>") return
var/list/types_list = list("Victor (male)", "Verity (female)", "Oscar (military)") //Victor is vox_sounds_male, Verity is vox_sounds, Oscar is vox_sounds_military
if(!is_banned_from(ckey, "Voice Announcements"))
types_list += "Use Microphone"
var/voxType = input(src, "Which voice?", "VOX") in types_list
if(voxType == "Use Microphone")
voice_announce()
return return
var/message = input(src, "WARNING: Misuse of this verb can result in you being job banned. More help is available in 'Announcement Help'", "Announcement", src.last_announcement) as text var/message = input(src, "WARNING: Misuse of this verb can result in you being job banned. More help is available in 'Announcement Help'", "Announcement", src.last_announcement) as text
last_announcement = message last_announcement = message
var/voxType = input(src, "Which voice?", "VOX") in list("Victor (male)", "Verity (female)", "Oscar (military)") //Victor is vox_sounds_male, Verity is vox_sounds, Oscar is vox_sounds_military if(!message || GLOB.announcing_vox > world.time)
if(!message || announcing_vox > world.time)
return return
if(incapacitated()) if(incapacitated())
@@ -130,7 +150,7 @@
to_chat(src, "<span class='notice'>These words are not available on the announcement system: [english_list(incorrect_words)].</span>") to_chat(src, "<span class='notice'>These words are not available on the announcement system: [english_list(incorrect_words)].</span>")
return return
announcing_vox = world.time + VOX_DELAY GLOB.announcing_vox = world.time + VOX_DELAY
log_game("[key_name(src)] made a vocal announcement with the following message: [message].") log_game("[key_name(src)] made a vocal announcement with the following message: [message].")

View File

@@ -14,7 +14,7 @@ $include resources.txt
SERVERNAME Space station 13 SERVERNAME Space station 13
## Server SQL name: This is the name used to identify the server to the SQL DB, distinct from SERVERNAME as it must be at most 32 characters. ## Server SQL name: This is the name used to identify the server to the SQL DB, distinct from SERVERNAME as it must be at most 32 characters.
#SERVERSQLNAME SERVERSQLNAME yogstation
## Put on byond hub: Uncomment this to put your server on the byond hub. ## Put on byond hub: Uncomment this to put your server on the byond hub.
#HUB #HUB
@@ -79,3 +79,6 @@ TICK_LIMIT_MC_INIT 500
## FEEDBACK_TABLEPREFIX SS13_ ## FEEDBACK_TABLEPREFIX SS13_
## Remove "SS13_" if you are using the standard schema file. ## Remove "SS13_" if you are using the standard schema file.
FEEDBACK_TABLEPREFIX SS13_ FEEDBACK_TABLEPREFIX SS13_
VOICE_ANNOUNCE_URL_BASE http://localhost/voice_announce/
VOICE_ANNOUNCE_DIR ../Yogstation.net/voice_announce_tmp

View File

@@ -86,3 +86,6 @@ SQL_ENABLED
## FEEDBACK_TABLEPREFIX SS13_ ## FEEDBACK_TABLEPREFIX SS13_
## Remove "SS13_" if you are using the standard schema file. ## Remove "SS13_" if you are using the standard schema file.
FEEDBACK_TABLEPREFIX erro_ FEEDBACK_TABLEPREFIX erro_
VOICE_ANNOUNCE_URL_BASE https://www.yogstation.net/voice_announce/
VOICE_ANNOUNCE_DIR data/voice_announcements

View File

@@ -268,6 +268,7 @@ const PageMain = (props, context) => {
callShuttleReasonMinLength, callShuttleReasonMinLength,
canBuyShuttles, canBuyShuttles,
canMakeAnnouncement, canMakeAnnouncement,
canMakeVoiceAnnouncement,
canMessageAssociates, canMessageAssociates,
canRecallShuttles, canRecallShuttles,
canRequestNuke, canRequestNuke,
@@ -380,6 +381,12 @@ const PageMain = (props, context) => {
onClick={() => act("makePriorityAnnouncement")} onClick={() => act("makePriorityAnnouncement")}
/>} />}
{!!canMakeVoiceAnnouncement && <Button
icon="bullhorn"
content="Make Voice Announcement"
onClick={() => act("makeVoiceAnnouncement")}
/>}
{!!canToggleEmergencyAccess && <Button.Confirm {!!canToggleEmergencyAccess && <Button.Confirm
icon="id-card-o" icon="id-card-o"
content={`${emergencyAccess ? "Disable" : "Enable"} Emergency Maintenance Access`} content={`${emergencyAccess ? "Disable" : "Enable"} Emergency Maintenance Access`}

View File

@@ -368,6 +368,7 @@
#include "code\datums\spawners_menu.dm" #include "code\datums\spawners_menu.dm"
#include "code\datums\verbs.dm" #include "code\datums\verbs.dm"
#include "code\datums\view.dm" #include "code\datums\view.dm"
#include "code\datums\voice_announcements.dm"
#include "code\datums\weakrefs.dm" #include "code\datums\weakrefs.dm"
#include "code\datums\world_topic.dm" #include "code\datums\world_topic.dm"
#include "code\datums\achievements\achievements.dm" #include "code\datums\achievements\achievements.dm"