diff --git a/code/__HELPERS/files.dm b/code/__HELPERS/files.dm
index c773b8e9b76..f8cb06fd762 100644
--- a/code/__HELPERS/files.dm
+++ b/code/__HELPERS/files.dm
@@ -72,7 +72,12 @@ GLOBAL_VAR_INIT(fileaccess_timer, 0)
#undef FTPDELAY
#undef ADMIN_FTPDELAY_MODIFIER
-/proc/pathwalk(path)
+/**
+ * Takes a directory and returns every file within every sub directory.
+ * If extensions_filter is provided then only files that end in that extension are given back.
+ * If extensions_filter is a list, any file that matches at least one entry is given back.
+ */
+/proc/pathwalk(path, extensions_filter)
var/list/jobs = list(path)
var/list/filenames = list()
@@ -82,9 +87,19 @@ GLOBAL_VAR_INIT(fileaccess_timer, 0)
for(var/new_filename in new_filenames)
// if filename ends in / it is a directory, append to currdir
if(findtext(new_filename, "/", -1))
- jobs += current_dir + new_filename
+ jobs += "[current_dir][new_filename]"
+ continue
+ // filename extension filtering
+ if(extensions_filter)
+ if(islist(extensions_filter))
+ for(var/allowed_extension in extensions_filter)
+ if(endswith(new_filename, allowed_extension))
+ filenames += "[current_dir][new_filename]"
+ break
+ else if(endswith(new_filename, extensions_filter))
+ filenames += "[current_dir][new_filename]"
else
- filenames += current_dir + new_filename
+ filenames += "[current_dir][new_filename]"
return filenames
/proc/pathflatten(path)
diff --git a/code/__HELPERS/text.dm b/code/__HELPERS/text.dm
index 91406650d20..1e39cd6eaf3 100644
--- a/code/__HELPERS/text.dm
+++ b/code/__HELPERS/text.dm
@@ -1191,3 +1191,8 @@ GLOBAL_LIST_INIT(binary, list("0","1"))
text2num(semver_regex.group[2]),
text2num(semver_regex.group[3]),
)
+
+/// Returns TRUE if the input_text ends with the ending
+/proc/endswith(input_text, ending)
+ var/input_length = LAZYLEN(ending)
+ return !!findtext(input_text, ending, -input_length)
diff --git a/code/controllers/subsystem/sounds.dm b/code/controllers/subsystem/sounds.dm
index 9067c077da7..2bc253c5af7 100644
--- a/code/controllers/subsystem/sounds.dm
+++ b/code/controllers/subsystem/sounds.dm
@@ -23,8 +23,12 @@ SUBSYSTEM_DEF(sounds)
/// higher reserve position - decremented and incremented to reserve sound channels, anything above this is reserved. The channel at this index is the highest unreserved channel.
var/channel_reserve_high
+ /// All valid sound files in the sound directory
+ var/list/all_sounds
+
/datum/controller/subsystem/sounds/Initialize()
setup_available_channels()
+ find_all_available_sounds()
return SS_INIT_SUCCESS
/datum/controller/subsystem/sounds/proc/setup_available_channels()
@@ -37,6 +41,26 @@ SUBSYSTEM_DEF(sounds)
channel_random_low = 1
channel_reserve_high = length(channel_list)
+/datum/controller/subsystem/sounds/proc/find_all_available_sounds()
+ all_sounds = list()
+ // Put more common extensions first to speed this up a bit
+ var/static/list/valid_file_extensions = list(
+ ".ogg",
+ ".wav",
+ ".mid",
+ ".midi",
+ ".mod",
+ ".it",
+ ".s3m",
+ ".xm",
+ ".oxm",
+ ".raw",
+ ".wma",
+ ".aiff",
+ )
+
+ all_sounds = pathwalk("sound/", valid_file_extensions)
+
/// Removes a channel from using list.
/datum/controller/subsystem/sounds/proc/free_sound_channel(channel)
var/text_channel = num2text(channel)
@@ -78,7 +102,7 @@ SUBSYSTEM_DEF(sounds)
CRASH("Attempted to reserve sound channel without datum using the managed proc.")
.= reserve_channel()
if(!.)
- return FALSE
+ CRASH("No more sound channels can be reserved")
var/text_channel = num2text(.)
using_channels[text_channel] = D
LAZYINITLIST(using_channels_by_datum[D])
diff --git a/code/modules/flufftext/Dreaming.dm b/code/modules/flufftext/Dreaming.dm
index 9c0ca9d33c5..06472f84be6 100644
--- a/code/modules/flufftext/Dreaming.dm
+++ b/code/modules/flufftext/Dreaming.dm
@@ -19,14 +19,94 @@
/mob/living/carbon/proc/dream()
set waitfor = FALSE
- var/list/dream_fragments = list()
+
+ var/datum/dream/chosen_dream = pick_weight(GLOB.dreams)
+
+ dreaming = TRUE
+ dream_sequence(chosen_dream.GenerateDream(src), chosen_dream)
+
+/**
+ * Displays the passed list of dream fragments to a sleeping carbon.
+ *
+ * Displays the first string of the passed dream fragments, then either ends the dream sequence
+ * or performs a callback on itself depending on if there are any remaining dream fragments to display.
+ *
+ * Arguments:
+ * * dream_fragments - A list of strings, in the order they will be displayed.
+ * * current_dream - The dream datum used for the current dream
+ */
+
+/mob/living/carbon/proc/dream_sequence(list/dream_fragments, datum/dream/current_dream)
+ if(stat != UNCONSCIOUS || HAS_TRAIT(src, TRAIT_CRITICAL_CONDITION))
+ dreaming = FALSE
+ current_dream.OnDreamEnd(src)
+ return
+ var/next_message = dream_fragments[1]
+ dream_fragments.Cut(1,2)
+
+ if(istype(next_message, /datum/callback))
+ var/datum/callback/something_happens = next_message
+ next_message = something_happens.InvokeAsync(src)
+
+ to_chat(src, span_notice("... [next_message] ..."))
+
+ if(LAZYLEN(dream_fragments))
+ var/next_wait = rand(10, 30)
+ if(current_dream.sleep_until_finished)
+ AdjustSleeping(next_wait)
+ addtimer(CALLBACK(src, PROC_REF(dream_sequence), dream_fragments, current_dream), next_wait)
+ else
+ dreaming = FALSE
+ current_dream.OnDreamEnd(src)
+
+//-------------------------
+// DREAM DATUMS
+
+GLOBAL_LIST_INIT(dreams, populate_dream_list())
+
+/proc/populate_dream_list()
+ var/list/output = list()
+ for(var/datum/dream/dream_type as anything in subtypesof(/datum/dream))
+ output[new dream_type] = initial(dream_type.weight)
+ return output
+
+/**
+ * Contains all the behavior needed to play a kind of dream.
+ * All dream types get randomly selected from based on weight when an appropriate mobs dreams.
+ */
+/datum/dream
+ /// The relative chance this dream will be randomly selected
+ var/weight = 0
+
+ /// Causes the mob to sleep long enough for the dream to finish if begun
+ var/sleep_until_finished = FALSE
+
+/**
+ * Called when beginning a new dream for the dreamer.
+ * Gives back a list of dream events. Events can be text or callbacks that return text.
+ */
+/datum/dream/proc/GenerateDream(mob/living/carbon/dreamer)
+ return list()
+
+/**
+ * Called when the dream ends or is interrupted.
+ */
+/datum/dream/proc/OnDreamEnd(mob/living/carbon/dreamer)
+ return
+
+/// The classic random dream of various words that might form a cohesive narrative, but usually wont
+/datum/dream/random
+ weight = 1000
+
+/datum/dream/random/GenerateDream(mob/living/carbon/dreamer)
var/list/custom_dream_nouns = list()
var/fragment = ""
- for(var/obj/item/bedsheet/sheet in loc)
+ for(var/obj/item/bedsheet/sheet in dreamer.loc)
custom_dream_nouns += sheet.dream_messages
- dream_fragments += "you see"
+ . = list()
+ . += "you see"
//Subject
if(custom_dream_nouns.len && prob(90))
@@ -40,7 +120,7 @@
fragment = replacetext(fragment, "%ADJECTIVE% ", "")
if(findtext(fragment, "%A% "))
fragment = "\a [replacetext(fragment, "%A% ", "")]"
- dream_fragments += fragment
+ . += fragment
//Verb
fragment = ""
@@ -51,10 +131,9 @@
else
fragment += "will "
fragment += pick(GLOB.verbs)
- dream_fragments += fragment
+ . += fragment
if(prob(25))
- dream_sequence(dream_fragments)
return
//Object
@@ -66,29 +145,38 @@
fragment = replacetext(fragment, "%ADJECTIVE% ", "")
if(findtext(fragment, "%A% "))
fragment = "\a [replacetext(fragment, "%A% ", "")]"
- dream_fragments += fragment
+ . += fragment
- dreaming = TRUE
- dream_sequence(dream_fragments)
+/// Dream plays a random sound at you, chosen from all sounds in the folder
+/datum/dream/hear_something
+ weight = 500
-/**
- * Displays the passed list of dream fragments to a sleeping carbon.
- *
- * Displays the first string of the passed dream fragments, then either ends the dream sequence
- * or performs a callback on itself depending on if there are any remaining dream fragments to display.
- *
- * Arguments:
- * * dream_fragments - A list of strings, in the order they will be displayed.
- */
+ var/reserved_sound_channel
-/mob/living/carbon/proc/dream_sequence(list/dream_fragments)
- if(stat != UNCONSCIOUS || HAS_TRAIT(src, TRAIT_CRITICAL_CONDITION))
- dreaming = FALSE
- return
- var/next_message = dream_fragments[1]
- dream_fragments.Cut(1,2)
- to_chat(src, span_notice("... [next_message] ..."))
- if(LAZYLEN(dream_fragments))
- addtimer(CALLBACK(src, PROC_REF(dream_sequence), dream_fragments), rand(10,30))
- else
- dreaming = FALSE
+/datum/dream/hear_something/New()
+ . = ..()
+ RegisterSignal(SSsounds, COMSIG_SUBSYSTEM_POST_INITIALIZE, PROC_REF(ReserveSoundChannel))
+
+/datum/dream/hear_something/GenerateDream(mob/living/carbon/dreamer)
+ . = ..()
+ . += pick("you wind up a toy", "you hear something strange", "you pick out a record to play", "you hit shuffle on your music player")
+ . += CALLBACK(src, PROC_REF(PlayRandomSound))
+ . += "it reminds you of something"
+
+/datum/dream/hear_something/OnDreamEnd(mob/living/carbon/dreamer)
+ . = ..()
+ // In case we play some long ass music track
+ addtimer(CALLBACK(src, PROC_REF(StopSound), dreamer), 5 SECONDS)
+
+/datum/dream/hear_something/proc/ReserveSoundChannel()
+ reserved_sound_channel = SSsounds.reserve_sound_channel(src)
+ UnregisterSignal(SSsounds, COMSIG_SUBSYSTEM_POST_INITIALIZE)
+
+/datum/dream/hear_something/proc/PlayRandomSound(mob/living/carbon/dreamer)
+ var/sound/random_sound = sound(pick(SSsounds.all_sounds), channel=reserved_sound_channel)
+ random_sound.status = SOUND_STREAM
+ SEND_SOUND(dreamer, random_sound)
+ return "you hear something you weren't expecting!"
+
+/datum/dream/hear_something/proc/StopSound(mob/living/carbon/dreamer)
+ SEND_SOUND(dreamer, sound(channel=reserved_sound_channel))