diff --git a/.vscode/settings.json b/.vscode/settings.json index f95b8f7cc6b2..deb10825250a 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -7,12 +7,9 @@ "tgui/.yarn": true, "tgui/.pnp.*": true }, - "workbench.editorAssociations": [ - { - "filenamePattern": "*.dmi", - "viewType": "imagePreview.previewEditor" - } - ], + "workbench.editorAssociations": { + "*.dmi": "imagePreview.previewEditor" + }, "files.eol": "\n", "gitlens.advanced.blame.customArguments": ["-w"] } diff --git a/code/__DEFINES/sound.dm b/code/__DEFINES/sound.dm index 0a10f8c3a6d1..44dfd9f5eb91 100644 --- a/code/__DEFINES/sound.dm +++ b/code/__DEFINES/sound.dm @@ -8,11 +8,12 @@ #define CHANNEL_AMBIENCE 1018 #define CHANNEL_BUZZ 1017 #define CHANNEL_BICYCLE 1016 +#define CHANNEL_VOICE_ANNOUNCE 1015 //THIS SHOULD ALWAYS BE THE LOWEST ONE! //KEEP IT UPDATED -#define CHANNEL_HIGHEST_AVAILABLE 1015 +#define CHANNEL_HIGHEST_AVAILABLE 1014 #define MAX_INSTRUMENT_CHANNELS (128 * 6) diff --git a/code/__HELPERS/files.dm b/code/__HELPERS/files.dm index 960fe6c971a2..17d92cc1afb2 100644 --- a/code/__HELPERS/files.dm +++ b/code/__HELPERS/files.dm @@ -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()) log_admin_private("BROWSEFILES: Admin proc call blocked") message_admins("BROWSEFILES: Admin proc call blocked") diff --git a/code/controllers/configuration/entries/general.dm b/code/controllers/configuration/entries/general.dm index 49e0c6b6e98b..1230d5899dec 100644 --- a/code/controllers/configuration/entries/general.dm +++ b/code/controllers/configuration/entries/general.dm @@ -274,6 +274,10 @@ /datum/config_entry/string/invoke_youtubedl 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/see_own_notes //Can players see their own admin notes diff --git a/code/controllers/subsystem/communications.dm b/code/controllers/subsystem/communications.dm index 894e8844db5c..85b6df1ad34b 100644 --- a/code/controllers/subsystem/communications.dm +++ b/code/controllers/subsystem/communications.dm @@ -7,6 +7,7 @@ SUBSYSTEM_DEF(communications) var/silicon_message_cooldown var/nonsilicon_message_cooldown + var/last_voice_announce_open = 0 /datum/controller/subsystem/communications/proc/can_announce(mob/living/user, is_silicon) if(is_silicon && silicon_message_cooldown > world.time) diff --git a/code/datums/voice_announcements.dm b/code/datums/voice_announcements.dm new file mode 100644 index 000000000000..04cc6257b121 --- /dev/null +++ b/code/datums/voice_announcements.dm @@ -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, "") + 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. ((CANCEL))") + + 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 [get_area_name(user, TRUE)].", "[user.real_name]", 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) diff --git a/code/datums/world_topic.dm b/code/datums/world_topic.dm index ff837386395f..1e1438eaea3a 100644 --- a/code/datums/world_topic.dm +++ b/code/datums/world_topic.dm @@ -174,6 +174,41 @@ 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 keyword = "status" diff --git a/code/game/machinery/computer/communications.dm b/code/game/machinery/computer/communications.dm index fbcf30e78554..a5f162ce3db1 100755 --- a/code/game/machinery/computer/communications.dm +++ b/code/game/machinery/computer/communications.dm @@ -161,6 +161,10 @@ if (!authenticated_as_silicon_or_captain(usr)) return make_announcement(usr) + if ("makeVoiceAnnouncement") + if (!authenticated_as_non_silicon_captain(usr)) + return + make_voice_announcement(usr) if ("messageAssociates") if (!authenticated_as_non_silicon_captain(usr)) return @@ -343,6 +347,7 @@ if (STATE_MAIN) data["canBuyShuttles"] = can_buy_shuttles(user) data["canMakeAnnouncement"] = FALSE + data["canMakeVoiceAnnouncement"] = FALSE data["canMessageAssociates"] = FALSE data["canRecallShuttles"] = !issilicon(user) data["canRequestNuke"] = FALSE @@ -381,6 +386,7 @@ data["alertLevelTick"] = alert_level_tick data["canMakeAnnouncement"] = TRUE + data["canMakeVoiceAnnouncement"] = ishuman(user) data["canSetAlertLevel"] = issilicon(user) ? "NO_SWIPE_NEEDED" : "SWIPE_NEEDED" if (SSshuttle.emergency.mode != SHUTTLE_IDLE && SSshuttle.emergency.mode != SHUTTLE_RECALL) @@ -481,6 +487,14 @@ SScommunications.make_announcement(user, is_ai, input) deadchat_broadcast(" made a priority announcement from [get_area_name(usr, TRUE)].", "[user.real_name]", user) +/obj/machinery/computer/communications/proc/make_voice_announcement(mob/living/user) + if(!SScommunications.can_announce(user, FALSE)) + to_chat(user, "Intercomms recharging. Please stand by.") + 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) var/datum/radio_frequency/frequency = SSradio.return_frequency(FREQ_STATUS_DISPLAYS) diff --git a/code/modules/admin/sql_ban_system.dm b/code/modules/admin/sql_ban_system.dm index 3480853aee8b..814cce00c729 100644 --- a/code/modules/admin/sql_ban_system.dm +++ b/code/modules/admin/sql_ban_system.dm @@ -252,7 +252,7 @@ output += "" //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, - "Abstract" = list("Appearance", "Emote", "OOC")) + "Abstract" = list("Appearance", "Emote", "OOC", "Voice Announcements")) for(var/department in headless_job_lists) output += "