mirror of
https://github.com/Dingo-Shenanigans/ReSkate.git
synced 2026-10-17 18:37:07 +02:00
Cap outfit packets and stop dropping recipients when a relay cannot be built. Add per-player relay limits for outfits, skater sound and voice, and a back-off for Steam IDs that keep failing to join. Throwdowns: only participants can write rows, turns and scores; a copy that fails to spawn no longer blocks the others. Throttle remote skater and skateboard re-creation, cap remote objects per session, and show developer and friend marks only for identities Steam vouches for. Route all connections through Steam's relays. Dedicated server: validate map changes, clean player names, keep vote cooldowns across reconnects, and cut ban labels on a character boundary.
445 lines
23 KiB
C++
445 lines
23 KiB
C++
#include "voice_chat.h"
|
|
#include "native_voice.h"
|
|
#include "Engine/Core/Platform/launcher_support.h"
|
|
#include "Engine/Game/Input/voice_input.h"
|
|
#include "Extension/UI/Overlay/overlay.h"
|
|
#include <Windows.h>
|
|
#include <xaudio2.h>
|
|
#include <wrl/client.h>
|
|
#include <algorithm>
|
|
#include <atomic>
|
|
#include <chrono>
|
|
#include <condition_variable>
|
|
#include <deque>
|
|
#include <filesystem>
|
|
#include <map>
|
|
#include <mutex>
|
|
#include <set>
|
|
#include <stdexcept>
|
|
#include <thread>
|
|
|
|
namespace dingosdk::multiplayer {
|
|
namespace {
|
|
using Clock = std::chrono::steady_clock;
|
|
constexpr UINT32 sample_rate = 48000;
|
|
constexpr std::size_t max_pcm_bytes = sample_rate;
|
|
template<class T> T symbol(HMODULE module, const char *name) {
|
|
const auto address = GetProcAddress(module, name);
|
|
if (!address) throw std::runtime_error(std::string("Steam voice export unavailable: ") + name);
|
|
#pragma warning(push)
|
|
#pragma warning(disable:4191)
|
|
return reinterpret_cast<T>(address);
|
|
#pragma warning(pop)
|
|
}
|
|
struct SteamVoice {
|
|
void *user{};
|
|
void (*start)(void *){};
|
|
void (*stop)(void *){};
|
|
int (*get)(void *, bool, void *, std::uint32_t, std::uint32_t *, bool, void *,
|
|
std::uint32_t, std::uint32_t *, std::uint32_t){};
|
|
int (*decode)(void *, const void *, std::uint32_t, void *, std::uint32_t, std::uint32_t *, std::uint32_t){};
|
|
void open() {
|
|
if (user) return;
|
|
if (launcher::offline_mode()) throw std::runtime_error("Multiplayer is unavailable in offline mode. Start Steam and relaunch ReSkate.");
|
|
const auto module = GetModuleHandleW(L"steam_api64.dll");
|
|
if (!module) throw std::runtime_error("Start ReSkate through Steam to use voice chat.");
|
|
std::wstring path(32768, L'\0');
|
|
const auto length = GetModuleFileNameW(module, path.data(), static_cast<DWORD>(path.size()));
|
|
if (!length || length >= path.size()) throw std::runtime_error("Cannot verify Steam voice library.");
|
|
path.resize(length);
|
|
launcher::validate_steam_api_file(std::filesystem::path(path));
|
|
if (!symbol<int (*)()>(module, "SteamAPI_GetHSteamUser")())
|
|
throw std::runtime_error("Steam is not initialized for voice chat.");
|
|
start = symbol<decltype(start)>(module, "SteamAPI_ISteamUser_StartVoiceRecording");
|
|
stop = symbol<decltype(stop)>(module, "SteamAPI_ISteamUser_StopVoiceRecording");
|
|
get = symbol<decltype(get)>(module, "SteamAPI_ISteamUser_GetVoice");
|
|
decode = symbol<decltype(decode)>(module, "SteamAPI_ISteamUser_DecompressVoice");
|
|
user = symbol<void *(*)()>(module, "SteamAPI_SteamUser_v023")();
|
|
if (!user) throw std::runtime_error("Steam Voice is unavailable.");
|
|
}
|
|
};
|
|
// Steam's decoder reads bytes another player sent. A fault inside it is that one packet lost,
|
|
// not the game: the call is guarded like the other native calls made with remote data.
|
|
int decode_voice(const SteamVoice &steam, const void *in, std::uint32_t in_size, void *out, std::uint32_t out_size,
|
|
std::uint32_t *written, std::uint32_t rate) noexcept {
|
|
__try {
|
|
return steam.decode(steam.user, in, in_size, out, out_size, written, rate);
|
|
} __except (EXCEPTION_EXECUTE_HANDLER) {
|
|
return -1;
|
|
}
|
|
}
|
|
struct PcmBuffer {
|
|
std::vector<std::uint8_t> bytes;
|
|
std::atomic<bool> done{};
|
|
};
|
|
struct Speaker final : IXAudio2VoiceCallback {
|
|
IXAudio2SourceVoice *voice{};
|
|
std::uint64_t epoch{};
|
|
float radius{}, gain = 1.f;
|
|
Clock::time_point received{};
|
|
std::deque<std::unique_ptr<PcmBuffer>> buffers;
|
|
std::atomic<bool> failed{};
|
|
NativeVoicePlayback *native{};
|
|
std::uint64_t id{}, generation{};
|
|
~Speaker() {
|
|
if (voice) voice->DestroyVoice();
|
|
if (native) native->remove(id, generation);
|
|
}
|
|
void STDMETHODCALLTYPE OnVoiceProcessingPassStart(UINT32) override {}
|
|
void STDMETHODCALLTYPE OnVoiceProcessingPassEnd() override {}
|
|
void STDMETHODCALLTYPE OnStreamEnd() override {}
|
|
void STDMETHODCALLTYPE OnBufferStart(void *) override {}
|
|
void STDMETHODCALLTYPE OnBufferEnd(void *context) override {
|
|
static_cast<PcmBuffer *>(context)->done.store(true, std::memory_order_release);
|
|
}
|
|
void STDMETHODCALLTYPE OnLoopEnd(void *) override {}
|
|
void STDMETHODCALLTYPE OnVoiceError(void *, HRESULT) override { failed.store(true); }
|
|
void collect() {
|
|
std::erase_if(buffers, [](const auto &buffer) { return buffer->done.load(std::memory_order_acquire); });
|
|
}
|
|
bool submit(std::vector<std::uint8_t> pcm) {
|
|
collect();
|
|
std::size_t queued{};
|
|
for (const auto &buffer : buffers) queued += buffer->bytes.size();
|
|
if (buffers.size() >= 16 || queued + pcm.size() > max_pcm_bytes) return false;
|
|
auto buffer = std::make_unique<PcmBuffer>();
|
|
buffer->bytes = std::move(pcm);
|
|
XAUDIO2_BUFFER submission{};
|
|
submission.AudioBytes = static_cast<UINT32>(buffer->bytes.size());
|
|
submission.pAudioData = buffer->bytes.data();
|
|
submission.pContext = buffer.get();
|
|
if (FAILED(voice->SubmitSourceBuffer(&submission))) return false;
|
|
buffers.push_back(std::move(buffer));
|
|
return true;
|
|
}
|
|
};
|
|
struct Playback {
|
|
NativeVoicePlayback &native;
|
|
Microsoft::WRL::ComPtr<IXAudio2> engine;
|
|
IXAudio2MasteringVoice *master{};
|
|
std::map<std::uint64_t, std::unique_ptr<Speaker>> speakers;
|
|
explicit Playback(NativeVoicePlayback &value) : native(value) {}
|
|
~Playback() { close(); }
|
|
void close() {
|
|
speakers.clear();
|
|
if (master) { master->DestroyVoice(); master = nullptr; }
|
|
engine.Reset();
|
|
}
|
|
void open() {
|
|
if (engine) return;
|
|
if (FAILED(XAudio2Create(&engine))) throw std::runtime_error("Cannot start voice playback.");
|
|
if (FAILED(engine->CreateMasteringVoice(&master, 2, sample_rate, 0, nullptr, nullptr, AudioCategory_Communications))) {
|
|
engine.Reset();
|
|
throw std::runtime_error("No voice playback device is available.");
|
|
}
|
|
}
|
|
Speaker &speaker(std::uint64_t id, std::uint64_t epoch, std::uint64_t generation, bool use_native) {
|
|
auto &entry = speakers[id];
|
|
if (entry && (entry->epoch != epoch || entry->failed.load() || (entry->native != nullptr) != use_native)) entry.reset();
|
|
if (!entry) {
|
|
auto next = std::make_unique<Speaker>();
|
|
next->id = id; next->generation = generation;
|
|
if (use_native) next->native = &native;
|
|
else {
|
|
open();
|
|
WAVEFORMATEX format{WAVE_FORMAT_PCM, 1, sample_rate, sample_rate * 2, 2, 16, 0};
|
|
if (FAILED(engine->CreateSourceVoice(&next->voice, &format, 0, 1.f, next.get())))
|
|
throw std::runtime_error("Cannot create a player's voice stream.");
|
|
if (FAILED(next->voice->Start())) throw std::runtime_error("Cannot start a player's voice stream.");
|
|
}
|
|
next->epoch = epoch;
|
|
entry = std::move(next);
|
|
}
|
|
return *entry;
|
|
}
|
|
};
|
|
bool focused() {
|
|
DWORD process{};
|
|
GetWindowThreadProcessId(GetForegroundWindow(), &process);
|
|
return process == GetCurrentProcessId() && overlay::keyboard_shortcuts_allowed();
|
|
}
|
|
// A global speaker (0) is heard everywhere at full volume; otherwise the
|
|
// listener's own hearing distance decides, and with proximity off they hear
|
|
// everyone at full volume.
|
|
float radius_for(float remote, const VoiceSettings &settings) {
|
|
return remote > 0.f && settings.proximity ? settings.distance : 0.f;
|
|
}
|
|
std::array<float, 2> stereo(const VoiceScene &scene, const VoicePeer &peer) {
|
|
return voice_stereo(scene.ear_position, scene.ear_right, peer.root.position);
|
|
}
|
|
}
|
|
struct VoiceChat::Impl {
|
|
NativeVoicePlayback native;
|
|
mutable std::mutex mutex;
|
|
std::condition_variable wake;
|
|
VoiceModel state;
|
|
VoiceScene scene;
|
|
Clock::time_point updated{};
|
|
std::uint64_t generation{};
|
|
std::set<std::uint64_t> muted;
|
|
std::map<std::uint64_t, float> volumes;
|
|
struct Incoming { std::uint64_t id, epoch, generation; VoiceData voice; Clock::time_point time; };
|
|
struct Outgoing { std::uint64_t generation; VoiceData voice; Clock::time_point time; };
|
|
std::deque<Incoming> incoming;
|
|
std::deque<Outgoing> outgoing;
|
|
std::jthread worker;
|
|
~Impl() {
|
|
worker.request_stop(); wake.notify_all();
|
|
if (worker.joinable()) worker.join();
|
|
}
|
|
void run(std::stop_token stop_token) noexcept {
|
|
const auto com = CoInitializeEx(nullptr, COINIT_MULTITHREADED);
|
|
SteamVoice steam;
|
|
Playback playback(native);
|
|
VoicePushToTalk push_to_talk;
|
|
bool recording{}, draining{}, forward_tail{};
|
|
std::uint64_t current_generation{};
|
|
Clock::time_point retry{};
|
|
std::array<std::uint8_t, 65536> compressed{};
|
|
while (!stop_token.stop_requested()) {
|
|
VoiceScene current;
|
|
VoiceSettings settings;
|
|
std::set<std::uint64_t> blocked;
|
|
std::map<std::uint64_t, float> gains;
|
|
std::deque<Incoming> packets;
|
|
Clock::time_point refreshed;
|
|
std::uint64_t revision{};
|
|
{
|
|
std::unique_lock lock(mutex);
|
|
wake.wait_for(lock, std::chrono::milliseconds(20));
|
|
current = scene; settings = state.settings; refreshed = updated;
|
|
blocked = muted; gains = volumes; revision = generation;
|
|
packets.swap(incoming);
|
|
}
|
|
const auto now = Clock::now();
|
|
const bool active = settings.enabled && current.active && current.policy.allowed &&
|
|
now - refreshed < std::chrono::milliseconds(300);
|
|
const bool foreground = focused();
|
|
const bool can_transmit = active && foreground && !current.peers.empty();
|
|
if (revision != current_generation) push_to_talk = {};
|
|
ControllerInput controller;
|
|
if (can_transmit && !settings.open_mic && settings.controller_combo)
|
|
DingoSDKOverlayReadControllerInput(&controller);
|
|
const bool held = push_to_talk.update(settings,
|
|
can_transmit && settings.push_to_talk && (GetAsyncKeyState(settings.push_to_talk) & 0x8000) != 0,
|
|
controller, !can_transmit || settings.open_mic);
|
|
const bool wanted = can_transmit && (settings.open_mic || held) && settings.microphone > 0.f;
|
|
const auto player_gain = [&](std::uint64_t id) {
|
|
const auto found = gains.find(id);
|
|
return found == gains.end() ? 1.f : found->second;
|
|
};
|
|
bool ready{};
|
|
std::string detail = settings.enabled ? "Join a ReSkate session and load a level to use voice." : "Voice chat is off.";
|
|
if (!current.policy.allowed) detail = "Voice chat is disabled by the host.";
|
|
std::vector<VoicePlayer> players;
|
|
try {
|
|
if (revision != current_generation) {
|
|
playback.speakers.clear();
|
|
if (recording) { steam.stop(steam.user); recording = false; draining = true; }
|
|
forward_tail = false;
|
|
current_generation = revision;
|
|
}
|
|
if (active && now >= retry) {
|
|
steam.open();
|
|
if (!native.available()) playback.open();
|
|
}
|
|
ready = active && steam.user && (native.available() || playback.engine);
|
|
if (wanted && ready && !recording && !draining) { steam.start(steam.user); recording = true; }
|
|
if ((!wanted || !ready) && recording) {
|
|
steam.stop(steam.user); recording = false; draining = true;
|
|
forward_tail = ready && active && foreground && settings.microphone > 0.f;
|
|
}
|
|
if (!active || !foreground) forward_tail = false;
|
|
if (recording || draining) {
|
|
std::uint32_t count{};
|
|
const auto result = steam.get(steam.user, true, compressed.data(), static_cast<std::uint32_t>(compressed.size()),
|
|
&count, false, nullptr, 0, nullptr, 0);
|
|
if (result == 2) { draining = false; recording = false; }
|
|
else if (result != 0 && result != 3) throw std::runtime_error("Steam microphone error " + std::to_string(result) + ". Check Steam's voice input device.");
|
|
if (result == 0 && count && count <= max_voice_bytes && (recording || forward_tail) &&
|
|
active && foreground && !current.peers.empty()) {
|
|
VoiceData data;
|
|
data.distance = settings.proximity ? settings.distance : 0.f;
|
|
data.gain = settings.microphone;
|
|
data.policy_revision = current.policy.revision;
|
|
data.bytes.assign(compressed.begin(), compressed.begin() + count);
|
|
std::lock_guard lock(mutex);
|
|
if (generation == revision && state.settings.enabled) {
|
|
while (outgoing.size() >= 8) outgoing.pop_front();
|
|
outgoing.push_back({revision, std::move(data), now});
|
|
}
|
|
}
|
|
}
|
|
if (!ready) playback.speakers.clear();
|
|
if (ready) {
|
|
for (auto &packet : packets) {
|
|
if (packet.generation != revision || !current.policy.accepts(packet.voice) ||
|
|
now - packet.time > std::chrono::milliseconds(250) || blocked.contains(packet.id)) continue;
|
|
const auto peer = std::find_if(current.peers.begin(), current.peers.end(), [&](const auto &p) { return p.id == packet.id && p.epoch == packet.epoch; });
|
|
if (peer == current.peers.end() || voice_gain(current.listener.position, peer->root.position,
|
|
radius_for(packet.voice.distance, settings)) <= 0.f) continue;
|
|
const bool use_native = native.available() && radius_for(packet.voice.distance, settings) > 0.f;
|
|
std::vector<std::uint8_t> pcm(max_pcm_bytes);
|
|
std::uint32_t size{};
|
|
if (decode_voice(steam, packet.voice.bytes.data(), static_cast<std::uint32_t>(packet.voice.bytes.size()),
|
|
pcm.data(), static_cast<std::uint32_t>(pcm.size()), &size,
|
|
use_native ? native_voice_rate : sample_rate) != 0 ||
|
|
!size || size > pcm.size() || size % 2) continue;
|
|
pcm.resize(size);
|
|
auto &speaker = playback.speaker(packet.id, packet.epoch, revision, use_native);
|
|
speaker.radius = packet.voice.distance;
|
|
speaker.gain = packet.voice.gain;
|
|
speaker.received = now;
|
|
const auto gain = settings.volume * player_gain(packet.id) * speaker.gain * voice_gain(current.listener.position, peer->root.position,
|
|
radius_for(speaker.radius, settings));
|
|
if (speaker.native) {
|
|
auto position = peer->root.position;
|
|
position[1] += 1.5f;
|
|
native.position(packet.id, revision, position, gain);
|
|
native.submit(packet.id, revision, pcm);
|
|
} else {
|
|
const auto matrix = stereo(current, *peer);
|
|
speaker.voice->SetVolume(gain);
|
|
speaker.voice->SetOutputMatrix(playback.master, 1, 2, matrix.data());
|
|
speaker.submit(std::move(pcm));
|
|
}
|
|
}
|
|
std::erase_if(playback.speakers, [&](const auto &item) {
|
|
const auto peer = std::find_if(current.peers.begin(), current.peers.end(), [&](const auto &p) { return p.id == item.first && p.epoch == item.second->epoch; });
|
|
return peer == current.peers.end() || blocked.contains(item.first) ||
|
|
now - item.second->received > std::chrono::seconds(1) ||
|
|
voice_gain(current.listener.position, peer->root.position, radius_for(item.second->radius, settings)) <= 0.f ||
|
|
(item.second->native && !native.available());
|
|
});
|
|
for (const auto &peer : current.peers) {
|
|
const auto found = playback.speakers.find(peer.id);
|
|
bool speaking{};
|
|
if (found != playback.speakers.end()) {
|
|
auto &speaker = *found->second;
|
|
speaker.collect();
|
|
const auto gain = settings.volume * player_gain(peer.id) * speaker.gain *
|
|
voice_gain(current.listener.position, peer.root.position, radius_for(speaker.radius, settings));
|
|
if (speaker.native) {
|
|
auto position = peer.root.position;
|
|
position[1] += 1.5f;
|
|
native.position(peer.id, revision, position, gain);
|
|
speaking = now - speaker.received < std::chrono::milliseconds(250);
|
|
} else {
|
|
speaker.voice->SetVolume(gain);
|
|
const auto matrix = stereo(current, peer);
|
|
speaker.voice->SetOutputMatrix(playback.master, 1, 2, matrix.data());
|
|
speaking = !speaker.buffers.empty();
|
|
}
|
|
}
|
|
players.push_back({peer.id, blocked.contains(peer.id), speaking, player_gain(peer.id)});
|
|
}
|
|
detail = recording ? "Transmitting microphone audio." : !foreground ? "Microphone paused while a menu is open or the game is unfocused." :
|
|
current.peers.empty() ? "Waiting for another player in this level." :
|
|
settings.open_mic ? "Open microphone ready. Uses Steam's voice input device." : "Push-to-talk ready. Uses Steam's voice input device.";
|
|
detail += native.available() ? " Proximity playback: Frostbite." : " Playback: XAudio2 fallback.";
|
|
}
|
|
} catch (const std::exception &error) {
|
|
if (recording) { steam.stop(steam.user); recording = false; draining = true; }
|
|
playback.close(); ready = false; detail = error.what();
|
|
retry = now + std::chrono::seconds(2);
|
|
} catch (...) {
|
|
if (recording) { steam.stop(steam.user); recording = false; draining = true; }
|
|
playback.close(); ready = false; detail = "Voice chat failed. Disable and enable it to retry.";
|
|
retry = now + std::chrono::seconds(2);
|
|
}
|
|
{
|
|
std::lock_guard lock(mutex);
|
|
if (generation == revision) {
|
|
state.ready = ready; state.transmitting = recording;
|
|
state.status = std::move(detail); state.players = std::move(players);
|
|
}
|
|
}
|
|
}
|
|
if (recording) steam.stop(steam.user);
|
|
playback.close();
|
|
if (SUCCEEDED(com)) CoUninitialize();
|
|
}
|
|
};
|
|
VoiceChat::VoiceChat() : impl_(std::make_unique<Impl>()) {}
|
|
VoiceChat::~VoiceChat() = default;
|
|
void VoiceChat::configure(VoiceSettings settings) {
|
|
if (!settings.valid()) return;
|
|
std::lock_guard lock(impl_->mutex);
|
|
const auto &old = impl_->state.settings;
|
|
if (old.enabled != settings.enabled || old.open_mic != settings.open_mic ||
|
|
old.push_to_talk != settings.push_to_talk || old.controller_combo != settings.controller_combo ||
|
|
old.microphone != settings.microphone) {
|
|
++impl_->generation; impl_->incoming.clear(); impl_->outgoing.clear();
|
|
impl_->native.reset(impl_->generation);
|
|
impl_->state.transmitting = false;
|
|
}
|
|
impl_->state.settings = settings;
|
|
if (settings.enabled && !impl_->worker.joinable())
|
|
impl_->worker = std::jthread([p = impl_.get()](std::stop_token stop) { p->run(stop); });
|
|
impl_->wake.notify_all();
|
|
}
|
|
VoiceModel VoiceChat::model() const {
|
|
std::lock_guard lock(impl_->mutex);
|
|
auto result = impl_->state;
|
|
for (const auto &[id, gain] : impl_->volumes) {
|
|
const auto found = std::find_if(result.players.begin(), result.players.end(), [id](const auto &p) { return p.id == id; });
|
|
if (found == result.players.end()) result.players.push_back({id, false, false, gain});
|
|
else found->volume = gain;
|
|
}
|
|
for (const auto id : impl_->muted) {
|
|
const auto found = std::find_if(result.players.begin(), result.players.end(), [id](const auto &p) { return p.id == id; });
|
|
if (found == result.players.end()) result.players.push_back({id, true, false});
|
|
else { found->muted = true; found->speaking = false; }
|
|
}
|
|
return result;
|
|
}
|
|
void VoiceChat::mute(std::uint64_t id, bool muted) {
|
|
std::lock_guard lock(impl_->mutex);
|
|
if (muted) impl_->muted.insert(id); else impl_->muted.erase(id);
|
|
impl_->wake.notify_all();
|
|
}
|
|
void VoiceChat::volume(std::uint64_t id, float volume) {
|
|
if (!valid_voice_volume(volume)) return;
|
|
std::lock_guard lock(impl_->mutex);
|
|
impl_->volumes[id] = volume;
|
|
impl_->wake.notify_all();
|
|
}
|
|
void VoiceChat::update(VoiceScene scene) {
|
|
std::lock_guard lock(impl_->mutex);
|
|
if (impl_->scene.session != scene.session || impl_->scene.world != scene.world || impl_->scene.active != scene.active ||
|
|
impl_->scene.policy != scene.policy) {
|
|
++impl_->generation; impl_->incoming.clear(); impl_->outgoing.clear();
|
|
impl_->native.reset(impl_->generation);
|
|
impl_->state.transmitting = false;
|
|
}
|
|
impl_->state.allowed = scene.policy.allowed;
|
|
impl_->scene = std::move(scene);
|
|
impl_->updated = Clock::now();
|
|
}
|
|
void VoiceChat::reset() { update({}); impl_->wake.notify_all(); }
|
|
void VoiceChat::process_native(std::uintptr_t base) noexcept { impl_->native.process(base); }
|
|
void VoiceChat::receive(const Packet &packet) {
|
|
if (packet.kind != PacketKind::voice || !valid_voice(packet.voice)) return;
|
|
std::lock_guard lock(impl_->mutex);
|
|
if (!impl_->state.settings.enabled || !impl_->scene.active || !impl_->scene.policy.accepts(packet.voice) || impl_->muted.contains(packet.source) ||
|
|
packet.session != impl_->scene.session || packet.world != impl_->scene.world) return;
|
|
const auto peer = std::find_if(impl_->scene.peers.begin(), impl_->scene.peers.end(), [&](const auto &p) {
|
|
return p.id == packet.source && p.epoch == packet.epoch;
|
|
});
|
|
if (peer == impl_->scene.peers.end() || impl_->incoming.size() >= 128) return;
|
|
const auto count = std::count_if(impl_->incoming.begin(), impl_->incoming.end(), [&](const auto &p) { return p.id == packet.source; });
|
|
if (count >= 8) return;
|
|
impl_->incoming.push_back({packet.source, packet.epoch, impl_->generation, packet.voice, Clock::now()});
|
|
}
|
|
std::vector<VoiceData> VoiceChat::take_capture() {
|
|
std::lock_guard lock(impl_->mutex);
|
|
std::vector<VoiceData> result;
|
|
const auto now = Clock::now();
|
|
for (auto &packet : impl_->outgoing)
|
|
if (impl_->state.settings.enabled && impl_->scene.active && impl_->scene.policy.accepts(packet.voice) && packet.generation == impl_->generation &&
|
|
now - packet.time <= std::chrono::milliseconds(250)) result.push_back(std::move(packet.voice));
|
|
impl_->outgoing.clear();
|
|
return result;
|
|
}
|
|
}
|