Files
Techbot121andMaia 145cd4c1ff Fix interaction sounds not working (#6053)
## About The Pull Request

the arguments for `SEND_SOUND` were switched, so even if a sound for an
interaction was set it would not work.

I have also changed it to use playsound instead (similarly to the
existing sound emotes see
[code/datums/emotes.dm:141](https://github.com/Bubberstation/Bubberstation/blob/master/code/datums/emotes.dm#L141))
that way it doesn't play at the default full volume (ouch) and would
sound as you expect it.
Only downside to this is that this would be audible to  ghosts as well.
I can just revert that change and just leave it at  
`SEND_SOUND(mob, sound(sound_cache, volume = 50))` if necessary.

while writing this pr I was thinking, maybe I could've also set vary to
true so the pitch changes as well, but I've not tested that yet and
maybe it actually sounds horrible.
## Why It's Good For The Game

If sounds are added to interactions, they would actually work.
## Proof Of Testing
<details>
<summary>Screenshots/Videos</summary>


https://github.com/user-attachments/assets/2cc8fe04-b6b5-4f21-8cc3-bac1cf6309e0

</details>

## Changelog
🆑
fix: fixed interaction sounds not working (if there was one)
/🆑
2026-08-14 19:42:36 +02:00

276 lines
11 KiB
Plaintext

GLOBAL_LIST_EMPTY_TYPED(interaction_instances, /datum/interaction)
/datum/interaction
/// The name to be displayed in the interaction menu for this interaction
var/name = "broken interaction"
/// The description of the interacton.
var/description = "broken"
/// If it can be done at a distance.
var/distance_allowed = FALSE
/// A list of possible messages displayed loaded by the JSON.
var/list/message = list()
/// A list of possible messages displayed directly to the USER.
var/list/user_messages = list()
/// A list of possible messages displayed directly to the TARGET.
var/list/target_messages = list()
/// What category this interaction will fall under in the menu.
var/category = INTERACTION_CAT_HIDE
/// Defines how we interact with ourselves or others.
var/usage = INTERACTION_OTHER
/// Does this interaction play a sound?
var/sound_use = FALSE
/// If it plays a sound, how far does it travel?
var/sound_range = 1
/// Stores the sound for later.
var/sound_cache = null
/// Is this lewd?
var/lewd = FALSE
/// What parts do WE need(IMPORTANT TO GET IT TO THE CORRECT DEFINE, ORGAN SLOT)?
var/list/user_required_parts = list()
/// What parts do they need(IMPORTANT TO GET IT TO THE CORRECT DEFINE, ORGAN SLOT)?
var/list/target_required_parts = list()
/// The amount of pleasure the target receives from this interaciton.
var/target_pleasure = 0
/// The amount of arousal the target receives from this interaction.
var/target_arousal = 0
/// The amount of pain the target receives.
var/target_pain = 0
/// The amount of pleasure the user receives.
var/user_pleasure = 0
/// The amount of arousal the user receives.
var/user_arousal = 0
/// The amount of pain the user receives.
var/user_pain = 0
/// A list of possible sounds.
var/list/sound_possible = list()
/// What requirements does this interaction have? See defines.
var/list/interaction_requires = list()
/// What color should the interaction button be?
var/color = "blue"
/// What sexuality preference do we display for.
var/sexuality = ""
/datum/interaction/proc/allow_act(mob/living/carbon/human/user, mob/living/carbon/human/target)
if(target == user && usage == INTERACTION_OTHER)
return FALSE
if(user_required_parts.len)
for(var/thing in user_required_parts)
var/obj/item/organ/genital/required_part = user.get_organ_slot(thing)
if(isnull(required_part))
return FALSE
if(!required_part.is_exposed())
return FALSE
if(target_required_parts.len)
for(var/thing in target_required_parts)
var/obj/item/organ/genital/required_part = target.get_organ_slot(thing)
if(isnull(required_part))
return FALSE
if(!required_part.is_exposed())
return FALSE
for(var/requirement in interaction_requires)
switch(requirement)
if(INTERACTION_REQUIRE_SELF_HAND)
if(!user.get_active_hand())
return FALSE
if(INTERACTION_REQUIRE_TARGET_HAND)
if(!target.get_active_hand())
return FALSE
else
CRASH("Unimplemented interaction requirement '[requirement]'")
return TRUE
/datum/interaction/proc/act(mob/living/carbon/human/user, mob/living/carbon/human/target, obj/body_relay = null)
if(!allow_act(user, target))
return
if(!message)
message_admins("Interaction had a null message list. '[name]'")
return
if(!islist(message) && istext(message))
message_admins("Deprecated message handling for '[name]'. Correct format is a list with one entry. This message will only show once.")
message = list(message)
var/msg = pick(message)
if(!isnull(body_relay))
msg = replacetext(msg, "%TARGET%", "\the [body_relay.name]")
// We replace %USER% with nothing because manual_emote already prepends it.
msg = trim(replacetext(replacetext(msg, "%TARGET%", "[target]"), "%USER%", ""), INTERACTION_MAX_CHAR)
if(lewd)
user.emote("subtle", null, msg, TRUE)
else
user.manual_emote(msg)
if(user_messages.len)
var/user_msg = pick(user_messages)
if(!isnull(body_relay))
user_msg = replacetext(user_msg, "%TARGET%", "\the [body_relay.name]")
user_msg = replacetext(replacetext(user_msg, "%TARGET%", "[target]"), "%USER%", "[user]")
to_chat(user, user_msg)
if(target_messages.len)
var/target_msg = pick(target_messages)
if(!isnull(body_relay))
target_msg = replacetext(target_msg, "%USER%", "Unknown")
target_msg = replacetext(replacetext(target_msg, "%TARGET%", "[target]"), "%USER%", "[user]")
to_chat(target, target_msg)
if(sound_use)
if(!sound_possible)
message_admins("Interaction has sound_use set to TRUE but does not set sound! '[name]'")
return
if(!islist(sound_possible) && istext(sound_possible))
message_admins("Deprecated sound handling for '[name]'. Correct format is a list with one entry. This message will only show once.")
sound_possible = list(sound_possible)
sound_cache = pick(sound_possible)
// playsound()'s range is always SOUND_RANGE (15) + extrarange, so we offset by -SOUND_RANGE to make
// extrarange effectively equal to the JSON-defined sound_range instead of stacking on top of it.
playsound(source = user, soundin = sound_cache, vol = 50, vary = FALSE, extrarange = sound_range - SOUND_RANGE, ignore_walls = FALSE, volume_preference = /datum/preference/numeric/volume/sound_emote)
if(lewd)
user.adjust_pleasure(user_pleasure)
user.adjust_arousal(user_arousal)
user.adjust_pain(user_pain)
target.adjust_pleasure(target_pleasure)
target.adjust_arousal(target_arousal)
target.adjust_pain(target_pain)
if(body_relay)
var/obj/lewd_portal_relay/body_portal_relay = body_relay
body_portal_relay.update_visuals()
/datum/interaction/proc/load_from_json(path)
var/fpath = path
if(!fexists(fpath))
message_admins("Attempted to load an interaction from json and the file does not exist")
qdel(src)
return FALSE
var/file = file(fpath)
var/list/json = json_load(file)
name = sanitize_text(json["name"])
description = sanitize_text(json["description"])
distance_allowed = sanitize_integer(json["distance_allowed"], 0, 1, 0)
message = sanitize_islist(json["message"], list("json error"))
category = sanitize_text(json["category"])
usage = sanitize_text(json["usage"])
sound_use = sanitize_integer(json["sound_use"], 0, 1, 0)
sound_range = sanitize_integer(json["sound_range"], 1, 7, 1)
sound_possible = sanitize_islist(json["sound_possible"], list("json error"))
interaction_requires = sanitize_islist(json["interaction_requires"], list())
color = sanitize_text(json["color"])
user_messages = sanitize_islist(json["user_messages"], list())
user_required_parts = sanitize_islist(json["user_required_parts"], list())
user_arousal = sanitize_integer(json["user_arousal"], 0, 100, 0)
user_pleasure = sanitize_integer(json["user_pleasure"], 0, 100, 0)
user_pain = sanitize_integer(json["user_pain"], 0, 100, 0)
target_messages = sanitize_islist(json["target_messages"], list())
target_required_parts = sanitize_islist(json["target_required_parts"], list())
target_arousal = sanitize_integer(json["target_arousal"], 0, 100, 0)
target_pleasure = sanitize_integer(json["target_pleasure"], 0, 100, 0)
target_pain = sanitize_integer(json["target_pain"], 0, 100, 0)
lewd = sanitize_integer(json["lewd"], 0, 1, 0)
sexuality = sanitize_text(json["sexuality"])
return TRUE
/datum/interaction/proc/json_save(path)
var/fpath = path
if(fexists(fpath))
fdel(fpath)
var/list/json = list(
"name" = name,
"description" = description,
"distance_allowed" = distance_allowed,
"message" = message,
"category" = category,
"usage" = usage,
"sound_use" = sound_use,
"sound_range" = sound_range,
"sound_possible" = sound_possible,
"interaction_requires" = interaction_requires,
"color" = color,
"user_messages" = user_messages,
"user_required_parts" = user_required_parts,
"user_arousal" = user_arousal,
"user_pleasure" = user_pleasure,
"user_pain" = user_pain,
"target_messages" = target_messages,
"target_required_parts" = target_required_parts,
"target_arousal" = target_arousal,
"target_pleasure" = target_pleasure,
"target_pain" = target_pain,
"lewd" = lewd,
"sexuality" = sexuality,
)
var/file = file(fpath)
WRITE_FILE(file, json_encode(json))
return TRUE
/mob/living/carbon/human/Initialize(mapload)
. = ..()
AddComponent(/datum/component/interactable)
/// Global loading procs
/proc/populate_interaction_instances()
for(var/spath in subtypesof(/datum/interaction))
var/datum/interaction/interaction = new spath()
GLOB.interaction_instances[interaction.name] = interaction
populate_interaction_jsons(INTERACTION_JSON_FOLDER)
/proc/populate_interaction_jsons(directory)
for(var/file in flist(directory))
if(flist(directory + file) && !findlasttext(directory + file, ".json"))
populate_interaction_instances(directory + file)
continue
if(findlasttext(directory + file, ".master.json")) // This is a master json which has special handling
populate_interaction_jsons_master(directory + file)
continue
var/datum/interaction/interaction = new()
if(interaction.load_from_json(directory + file))
GLOB.interaction_instances[interaction.name] = interaction
else message_admins("Error loading interaction from file: '[directory + file]'. Inform coders.")
/proc/populate_interaction_jsons_master(path)
if(!fexists(path))
message_admins("We are attempting to load an interaction master without the file existing! '[path]'")
return
var/file = file(path)
var/list/json = json_load(file)
for(var/iname in json)
if(GLOB.interaction_instances[iname])
message_admins("Interaction Master '[path]' contained a duplicate interaction! '[iname]'")
continue
var/list/ijson = json[iname]
if(ijson["name"] != iname)
message_admins("Interaction Master '[path]' contained an invalid interaction! '[iname]'")
continue
var/datum/interaction/interaction = new()
interaction.distance_allowed = sanitize_integer(ijson["distance_allowed"], 0, 1, 0)
interaction.message = sanitize_islist(ijson["message"], list("json error"))
interaction.category = sanitize_text(ijson["category"])
interaction.usage = sanitize_text(ijson["usage"])
interaction.sound_use = sanitize_integer(ijson["sound_use"], 0, 1, 0)
interaction.sound_range = sanitize_integer(ijson["sound_range"], 1, 7, 1)
interaction.sound_possible = sanitize_islist(ijson["sound_possible"], list("json error"))
interaction.interaction_requires = sanitize_islist(ijson["interaction_requires"], list())
interaction.color = sanitize_text(ijson["color"])
interaction.user_messages = sanitize_islist(ijson["user_messages"], list())
interaction.user_required_parts = sanitize_islist(ijson["user_required_parts"], list())
interaction.user_arousal = sanitize_integer(ijson["user_arousal"], 0, 100, 0)
interaction.user_pleasure = sanitize_integer(ijson["user_pleasure"], 0, 100, 0)
interaction.user_pain = sanitize_integer(ijson["user_pain"], 0, 100, 0)
interaction.target_messages = sanitize_islist(ijson["target_messages"], list())
interaction.target_required_parts = sanitize_islist(ijson["target_required_parts"], list())
interaction.target_arousal = sanitize_integer(ijson["target_arousal"], 0, 100, 0)
interaction.target_pleasure = sanitize_integer(ijson["target_pleasure"], 0, 100, 0)
interaction.target_pain = sanitize_integer(ijson["target_pain"], 0, 100, 0)
interaction.lewd = sanitize_integer(ijson["lewd"], 0, 1, 0)
interaction.sexuality = sanitize_text(ijson["sexuality"])
GLOB.interaction_instances[iname] = interaction
ADMIN_VERB(reload_interactions, R_DEBUG, "Reload Interactions", "Force reload interactions.", ADMIN_CATEGORY_DEBUG)
populate_interaction_instances()