From 0da16f99373ccbbc27b075fb1fe968102008c45c Mon Sep 17 00:00:00 2001 From: SFENCE Date: Sat, 3 Feb 2024 18:47:42 +0100 Subject: [PATCH] Send sound to players which comes to hear distance. --- builtin/game/features.lua | 1 + doc/lua_api.md | 14 ++- games/devtest/mods/soundstuff/jukebox.lua | 32 ++++-- src/network/serverpackethandler.cpp | 33 +++++- src/script/common/c_content.cpp | 1 + src/server.cpp | 127 +++++++++++++++++++--- src/server.h | 13 +++ src/sound.h | 3 + 8 files changed, 193 insertions(+), 31 deletions(-) diff --git a/builtin/game/features.lua b/builtin/game/features.lua index 22bf1859d..a09eef2a9 100644 --- a/builtin/game/features.lua +++ b/builtin/game/features.lua @@ -42,6 +42,7 @@ core.features = { node_interaction_actor = true, moveresult_new_pos = true, override_item_remove_fields = true, + sounds_updating = true, } function core.has_feature(arg) diff --git a/doc/lua_api.md b/doc/lua_api.md index ac2304bca..7b8323b7f 100644 --- a/doc/lua_api.md +++ b/doc/lua_api.md @@ -1101,6 +1101,11 @@ Table used to specify how a sound is played: -- Available since feature `sound_params_start_time`. + resend_time = 0.0, + -- Approximate playback duration (from `start_time` to end) in seconds. + -- This is needed to re-send sounds to new players in hearing distance. + -- Unused for looped sounds. + loop = false, -- If true, sound is played in a loop. @@ -1136,10 +1141,10 @@ Table used to specify how a sound is played: -- Can't be used together with `to_player`. max_hear_distance = 32, - -- Only play for players that are at most this far away when the sound - -- starts playing. + -- Only play for players that are at most this far away. -- Needs `pos` or `object` to be set. -- `32` is the default. + -- See `sounds_updating`. } ``` @@ -5448,6 +5453,9 @@ Utilities moveresult_new_pos = true, -- Allow removing definition fields in `minetest.override_item` override_item_remove_fields = true, + -- Sounds can be send to players which comes to hear distance. + -- Field `resend_time` added to sound parameters. (5.9.0) + sounds_updating = true, } ``` @@ -6648,6 +6656,8 @@ Sounds ------ * `minetest.sound_play(spec, parameters, [ephemeral])`: returns a handle + * Returned handle is positive number if function sucessufully + create sound and `ephernal` if `false`. * `spec` is a `SimpleSoundSpec` * `parameters` is a sound parameter table * `ephemeral` is a boolean (default: false) diff --git a/games/devtest/mods/soundstuff/jukebox.lua b/games/devtest/mods/soundstuff/jukebox.lua index 298697edf..b10d9a94d 100644 --- a/games/devtest/mods/soundstuff/jukebox.lua +++ b/games/devtest/mods/soundstuff/jukebox.lua @@ -15,6 +15,7 @@ local meta_keys = { "sparam.pitch", "sparam.fade", "sparam.start_time", + "sparam.resend_time", "sparam.loop", "sparam.pos", "sparam.object", @@ -40,7 +41,8 @@ local function get_all_metadata(meta) gain = meta:get_string("sparam.gain"), pitch = meta:get_string("sparam.pitch"), fade = meta:get_string("sparam.fade"), - start_time = meta:get_string("sparam.start_time"), + start_time = meta:get_string("sparam.start_time"), + resend_time = meta:get_string("sparam.resend_time"), loop = meta:get_string("sparam.loop"), pos = meta:get_string("sparam.pos"), object = meta:get_string("sparam.object"), @@ -88,7 +90,7 @@ local function show_formspec(pos, player) fs_add([[ formspec_version[6] - size[14,12] + size[14,13] ]]) -- SimpleSoundSpec @@ -120,17 +122,19 @@ local function show_formspec(pos, player) field[1.25,1;1,0.75;sparam.pitch;pitch;%s] field[2.50,1;1,0.75;sparam.fade;fade;%s] field[0,2.25;4,0.75;sparam.start_time;start_time;%s] - field[0,3.50;4,0.75;sparam.loop;loop;%s] - field[0,4.75;4,0.75;sparam.pos;pos;%s] - field[0,6.00;4,0.75;sparam.object;object;%s] - field[0,7.25;4,0.75;sparam.to_player;to_player;%s] - field[0,8.50;4,0.75;sparam.exclude_player;exclude_player;%s] - field[0,9.75;4,0.75;sparam.max_hear_distance;max_hear_distance;%s] + field[0,3.50;4,0.75;sparam.resend_time;resend_time;%s] + field[0,4.75;4,0.75;sparam.loop;loop;%s] + field[0,6.00;4,0.75;sparam.pos;pos;%s] + field[0,7.25;4,0.75;sparam.object;object;%s] + field[0,8.50;4,0.75;sparam.to_player;to_player;%s] + field[0,9.75;4,0.75;sparam.exclude_player;exclude_player;%s] + field[0,11.00;4,0.75;sparam.max_hear_distance;max_hear_distance;%s] container_end[] field_close_on_enter[sparam.gain;false] field_close_on_enter[sparam.pitch;false] field_close_on_enter[sparam.fade;false] field_close_on_enter[sparam.start_time;false] + field_close_on_enter[sparam.resend_time;false] field_close_on_enter[sparam.loop;false] field_close_on_enter[sparam.pos;false] field_close_on_enter[sparam.object;false] @@ -139,7 +143,8 @@ local function show_formspec(pos, player) field_close_on_enter[sparam.max_hear_distance;false] tooltip[sparam.object;Get a name with the Branding Iron.] ]], F(md.sparam.gain), F(md.sparam.pitch), F(md.sparam.fade), - F(md.sparam.start_time), F(md.sparam.loop), F(md.sparam.pos), + F(md.sparam.start_time), F(md.sparam.resend_time), + F(md.sparam.loop), F(md.sparam.pos), F(md.sparam.object), F(md.sparam.to_player), F(md.sparam.exclude_player), F(md.sparam.max_hear_distance))) @@ -192,7 +197,7 @@ local function show_formspec(pos, player) -- save and quit button fs_add([[ - button_exit[10.75,11;3,0.75;btn_save_quit;Save & Quit] + button_exit[10.75,11.5;3,0.75;btn_save_quit;Save & Quit] ]]) minetest.show_formspec(player:get_player_name(), "soundstuff:jukebox@"..pos:to_string(), @@ -216,6 +221,7 @@ minetest.register_node("soundstuff:jukebox", { meta:set_string("sparam.pitch", "") meta:set_string("sparam.fade", "") meta:set_string("sparam.start_time", "") + meta:set_string("sparam.resend_time", "") meta:set_string("sparam.loop", "") meta:set_string("sparam.pos", pos:to_string()) meta:set_string("sparam.object", "") @@ -274,6 +280,7 @@ minetest.register_on_player_receive_fields(function(player, formname, fields) pitch = tonumber(md.sparam.pitch), fade = tonumber(md.sparam.fade), start_time = tonumber(md.sparam.start_time), + resend_time = tonumber(md.sparam.resend_time), loop = minetest.is_yes(md.sparam.loop), pos = vector.from_string(md.sparam.pos), object = testtools.get_branded_object(md.sparam.object), @@ -287,9 +294,10 @@ minetest.register_on_player_receive_fields(function(player, formname, fields) "[soundstuff:jukebox] Playing sound: minetest.sound_play(%s, %s, %s)", string.format("{name=\"%s\", gain=%s, pitch=%s, fade=%s}", sss.name, sss.gain, sss.pitch, sss.fade), - string.format("{gain=%s, pitch=%s, fade=%s, start_time=%s, loop=%s, pos=%s, " + string.format("{gain=%s, pitch=%s, fade=%s, start_time=%s, resend_time=%s, loop=%s, pos=%s, " .."object=%s, to_player=\"%s\", exclude_player=\"%s\", max_hear_distance=%s}", - sparam.gain, sparam.pitch, sparam.fade, sparam.start_time, + sparam.gain, sparam.pitch, sparam.fade, + sparam.start_time, sparam.resend_time, sparam.loop, sparam.pos, sparam.object and "", sparam.to_player, sparam.exclude_player, sparam.max_hear_distance), diff --git a/src/network/serverpackethandler.cpp b/src/network/serverpackethandler.cpp index 2c6f024ee..ab7b3bd7e 100644 --- a/src/network/serverpackethandler.cpp +++ b/src/network/serverpackethandler.cpp @@ -1339,10 +1339,39 @@ void Server::handleCommand_RemovedSounds(NetworkPacket* pkt) if (i == m_playing_sounds.end()) continue; + session_t peer_id = pkt->getPeerId(); + ServerPlayingSound &psound = i->second; - psound.clients.erase(pkt->getPeerId()); - if (psound.clients.empty()) + psound.clients.erase(peer_id); + verbosestream << "Server: Sound " << id << " erase peer " + << peer_id << "." << std::endl; + if (!psound.allow_resend && psound.clients.empty()) { m_playing_sounds.erase(i); + verbosestream << "Server: Erase sound " << id << "." << std::endl; + } + else if (psound.allow_resend && !psound.spec.loop) { + if (psound.resend_time < m_playing_sounds_time) { + /* Sound resend time reached, done_clients do not need to be updated. */ + verbosestream << "Server: Done sound " << id << " for peer " + << peer_id << "." << std::endl; + continue; + } + + PlayerSAO *plrsao = getPlayerSAO(peer_id); + if (!plrsao) + continue; + bool pos_exists; + v3f pos = psound.getPos(m_env, &pos_exists); + assert(pos_exists); + + if (pos.getDistanceFrom(plrsao->getBasePosition()) <= psound.max_hear_distance) { + /* If client remove sound in hear distance, + we are sure, that sound has been fully played. */ + psound.done_clients.insert(peer_id); + verbosestream << "Server: Done sound " << id << " for peer " + << peer_id << "." << std::endl; + } + } } } diff --git a/src/script/common/c_content.cpp b/src/script/common/c_content.cpp index 988094b9f..7ac7d4bdc 100644 --- a/src/script/common/c_content.cpp +++ b/src/script/common/c_content.cpp @@ -1179,6 +1179,7 @@ void read_server_sound_params(lua_State *L, int index, getfloatfield(L, index, "fade", params.spec.fade); getfloatfield(L, index, "pitch", params.spec.pitch); getfloatfield(L, index, "start_time", params.spec.start_time); + getfloatfield(L, index, "resend_time", params.spec.resend_time); getboolfield(L, index, "loop", params.spec.loop); getfloatfield(L, index, "gain", params.gain); diff --git a/src/server.cpp b/src/server.cpp index 937cbe90a..7f763ce43 100644 --- a/src/server.cpp +++ b/src/server.cpp @@ -915,6 +915,65 @@ void Server::AsyncRunStep(float dtime, bool initial_step) } } + /* + Send sounds if needed + */ + { + m_playing_sounds_time += dtime; + + MutexAutoLock lock(m_env_mutex); + + ClientInterface::AutoLock clientlock(m_clients); + const RemoteClientMap &clients = m_clients.getClientList(); + + for (auto it = m_playing_sounds.begin(); it != m_playing_sounds.end(); it++) { + ServerPlayingSound &sound = it->second; + + if (!sound.allow_resend) + continue; + + if (!sound.spec.loop && (sound.resend_time < m_playing_sounds_time)) { + sound.allow_resend = false; + verbosestream << "Server: Marking sound " << it->first + << " as expired." << std::endl; + continue; + } + + // this should be never called for sound not attached to pos or object + bool pos_exists; + v3f pos = sound.getPos(m_env, &pos_exists); + assert(pos_exists); + + for (const auto &client_it : clients) { + RemoteClient *client = client_it.second; + + if (client->getState() < CS_DefinitionsSent) + continue; + + PlayerSAO *playersao = getPlayerSAO(client->peer_id); + if (!playersao) + continue; + + if (sound.clients.find(client->peer_id) != sound.clients.end()) + continue; + + if (sound.done_clients.find(client->peer_id) != sound.clients.end()) + continue; + + if (!sound.exclude_player.empty() && + sound.exclude_player == client->getName()) + continue; + + if (pos.getDistanceFrom(playersao->getBasePosition()) <= sound.max_hear_distance) { + SendSound(client->peer_id, it->first, sound, pos); + sound.clients.insert(client->peer_id); + verbosestream << "Server: Sound " << it->first << " add peer " + << client->peer_id << "." << std::endl; + } + } + } + } + /* Send queued-for-sending map edit events. */ @@ -2118,6 +2177,15 @@ void Server::SendActiveObjectMessages(session_t peer_id, const std::string &data m_clients.sendCustom(pkt.getPeerId(), reliable ? ccf.channel : 1, &pkt, reliable); } +void Server::SendSound(session_t peer_id, s32 sound_id, + const ServerPlayingSound ¶ms, const v3f &pos) +{ + NetworkPacket pkt(TOCLIENT_PLAY_SOUND, 0, peer_id); + createSoundPacket(pkt, sound_id, params, pos); + + Send(&pkt); +} + void Server::SendCSMRestrictionFlags(session_t peer_id) { NetworkPacket pkt(TOCLIENT_CSM_RESTRICTION_FLAGS, @@ -2159,6 +2227,8 @@ s32 Server::playSound(ServerPlayingSound ¶ms, bool ephemeral) if(pos_exists != (params.type != SoundLocation::Local)) return -1; + params.allow_resend = pos_exists; + // Filter destination clients std::vector dst_clients; if (!params.to_player.empty()) { @@ -2169,6 +2239,7 @@ s32 Server::playSound(ServerPlayingSound ¶ms, bool ephemeral) return -1; } dst_clients.push_back(player->getPeerId()); + params.allow_resend = false; } else { std::vector clients = m_clients.getClientIDs(); @@ -2193,7 +2264,10 @@ s32 Server::playSound(ServerPlayingSound ¶ms, bool ephemeral) } } - if(dst_clients.empty()) + // lets finish only for ephmeral sounds, non-looped sounds and + // sounds without resend_time if no clients in hear distance. + if (dst_clients.empty() && + (ephemeral || (params.spec.resend_time == 0 && !params.spec.loop))) return -1; // old clients will still use this, so pick a reserved ID (-1) @@ -2201,23 +2275,27 @@ s32 Server::playSound(ServerPlayingSound ¶ms, bool ephemeral) if (id == 0) return 0; - float gain = params.gain * params.spec.gain; - NetworkPacket pkt(TOCLIENT_PLAY_SOUND, 0); - pkt << id << params.spec.name << gain - << (u8) params.type << pos << params.object - << params.spec.loop << params.spec.fade << params.spec.pitch - << ephemeral << params.spec.start_time; + params.start_time = m_playing_sounds_time; + params.resend_time = m_playing_sounds_time + params.spec.resend_time; - const bool as_reliable = !ephemeral; + if (!dst_clients.empty()) { + NetworkPacket pkt(TOCLIENT_PLAY_SOUND, 0); + createSoundPacket(pkt, id, params, pos, ephemeral); - for (const session_t peer_id : dst_clients) { - if (!ephemeral) - params.clients.insert(peer_id); - m_clients.sendCustom(peer_id, 0, &pkt, as_reliable); + const bool as_reliable = !ephemeral; + + for (const session_t peer_id : dst_clients) { + if (!ephemeral) + params.clients.insert(peer_id); + m_clients.sendCustom(peer_id, 0, &pkt, as_reliable); + } } - if (!ephemeral) + if (!ephemeral) { m_playing_sounds[id] = std::move(params); + verbosestream << "Server:playSound: Create sound " + << id << "." << std::endl; + } return id; } void Server::stopSound(s32 handle) @@ -2237,6 +2315,9 @@ void Server::stopSound(s32 handle) // Remove sound reference m_playing_sounds.erase(it); + + verbosestream << "Server:stopSound: Stop sound " + << handle << "." << std::endl; } void Server::fadeSound(s32 handle, float step, float gain) @@ -2280,17 +2361,33 @@ void Server::stopAttachedSounds(session_t peer_id, sound.clients.erase(clients_it); // delete if client list empty - return sound.clients.empty(); + return !sound.allow_resend && sound.clients.empty(); }; for (auto it = m_playing_sounds.begin(); it != m_playing_sounds.end(); ) { - if (cb(it->first, it->second)) + if (cb(it->first, it->second)) { + // Remove sound reference + verbosestream << "Server:stopAttachedSounds: Stop sound " + << it->first << "." << std::endl; it = m_playing_sounds.erase(it); + } else ++it; } } +void Server::createSoundPacket(NetworkPacket &pkt, s32 sound_id, + const ServerPlayingSound ¶ms, const v3f &pos, bool ephemeral) +{ + float gain = params.gain * params.spec.gain; + float start_time = params.spec.start_time + + m_playing_sounds_time - params.start_time; + pkt << sound_id << params.spec.name << gain + << (u8) params.type << pos << params.object + << params.spec.loop << params.spec.fade << params.spec.pitch + << ephemeral << start_time; +} + void Server::sendRemoveNode(v3s16 p, std::unordered_set *far_players, float far_d_nodes) { diff --git a/src/server.h b/src/server.h index 16c1ea4cc..19414562e 100644 --- a/src/server.h +++ b/src/server.h @@ -119,6 +119,16 @@ struct ServerPlayingSound SoundSpec spec; + // server processing values + + // relative server time when sound has been added + // used for update spec.start_time when sending sound to new client + float start_time; + // hold time limit for sound sending to new clients + float resend_time; + bool allow_resend = false; + + std::unordered_set done_clients; // peer ids std::unordered_set clients; // peer ids }; @@ -242,6 +252,7 @@ class Server : public con::PeerHandler, public MapEventReceiver, // Stop all sounds attached to given objects, for a certain client void stopAttachedSounds(session_t peer_id, const std::vector &object_ids); + void createSoundPacket(NetworkPacket &pkt, s32 sound_id, const ServerPlayingSound ¶ms, const v3f &pos, bool ephemeral = false); // Envlock std::set getPlayerEffectivePrivs(const std::string &name); @@ -568,6 +579,7 @@ class Server : public con::PeerHandler, public MapEventReceiver, void SendActiveObjectRemoveAdd(RemoteClient *client, PlayerSAO *playersao); void SendActiveObjectMessages(session_t peer_id, const std::string &datas, bool reliable = true); + void SendSound(session_t peer_id, s32 sound_id, const ServerPlayingSound ¶ms, const v3f &pos); void SendCSMRestrictionFlags(session_t peer_id); /* @@ -733,6 +745,7 @@ class Server : public con::PeerHandler, public MapEventReceiver, std::unordered_map m_playing_sounds; s32 m_playing_sounds_id_last_used = 0; // positive values only s32 nextSoundId(); + float m_playing_sounds_time = 0.0f; ModStorageDatabase *m_mod_storage_database = nullptr; float m_mod_storage_save_timer = 10.0f; diff --git a/src/sound.h b/src/sound.h index afb9ac9ef..a9e4c7ad0 100644 --- a/src/sound.h +++ b/src/sound.h @@ -71,6 +71,9 @@ struct SoundSpec float fade = 0.0f; float pitch = 1.0f; float start_time = 0.0f; + // keep time defines a time windows, where sound cannot be removed + // and can be resend, takes no effect for looped sounds + float resend_time = 0.0f; bool loop = false; // If true, a local fallback (ie. from the user's sound pack) is used if the // sound-group does not exist.