diff --git a/src/Lavalink4NET.DSharpPlus.Nightly/DSharpPlusUtilities.cs b/src/Lavalink4NET.DSharpPlus.Nightly/DSharpPlusUtilities.cs
new file mode 100644
index 00000000..1085b93e
--- /dev/null
+++ b/src/Lavalink4NET.DSharpPlus.Nightly/DSharpPlusUtilities.cs
@@ -0,0 +1,57 @@
+namespace Lavalink4NET.DSharpPlus;
+
+using System;
+using System.Collections.Concurrent;
+using System.Reflection;
+using global::DSharpPlus;
+using global::DSharpPlus.AsyncEvents;
+
+///
+/// An utility for getting internal / private fields from DSharpPlus WebSocket Gateway Payloads.
+///
+public static partial class DSharpPlusUtilities
+{
+ ///
+ /// The internal "events" property info in .
+ ///
+ // https://github.com/DSharpPlus/DSharpPlus/blob/master/DSharpPlus/Clients/DiscordClient.cs#L37
+ private static readonly FieldInfo eventsField =
+ typeof(DiscordClient).GetField("events", BindingFlags.NonPublic | BindingFlags.Instance)!;
+
+ ///
+ /// Gets the internal "events" property value of the specified .
+ ///
+ /// the instance
+ /// the "events" value
+ public static ConcurrentDictionary GetEvents(this DiscordClient client)
+ => (ConcurrentDictionary)eventsField.GetValue(client)!;
+
+ ///
+ /// The internal "errorHandler" property info in .
+ ///
+ // https://github.com/DSharpPlus/DSharpPlus/blob/master/DSharpPlus/Clients/DiscordClient.cs#L41
+ private static readonly FieldInfo errorHandlerField =
+ typeof(DiscordClient).GetField("errorHandler", BindingFlags.NonPublic | BindingFlags.Instance)!;
+
+ ///
+ /// Gets the internal "errorHandler" property value of the specified .
+ ///
+ /// the instance
+ /// the "errorHandler" value
+ public static IClientErrorHandler GetErrorHandler(this DiscordClient client)
+ => (IClientErrorHandler)errorHandlerField.GetValue(client)!;
+
+ ///
+ /// The internal "Register" method info in .
+ ///
+ // https://github.com/DSharpPlus/DSharpPlus/blob/master/DSharpPlus/AsyncEvents/AsyncEvent.cs#L14
+ private static readonly MethodInfo asyncEventRegisterMethod =
+ typeof(AsyncEvent).GetMethod("Register", BindingFlags.NonPublic | BindingFlags.Instance, [typeof(Delegate)])!;
+
+ ///
+ /// Calls the internal "Register" method of the spedificed
+ ///
+ /// the instance
+ /// the event to register
+ public static void Register(this AsyncEvent asyncEvent, Delegate @delegate) => asyncEventRegisterMethod.Invoke(asyncEvent, [@delegate]);
+}
diff --git a/src/Lavalink4NET.DSharpPlus.Nightly/DiscordClientWrapper.cs b/src/Lavalink4NET.DSharpPlus.Nightly/DiscordClientWrapper.cs
new file mode 100644
index 00000000..57428a5c
--- /dev/null
+++ b/src/Lavalink4NET.DSharpPlus.Nightly/DiscordClientWrapper.cs
@@ -0,0 +1,241 @@
+namespace Lavalink4NET.DSharpPlus;
+
+using System;
+using System.Collections.Immutable;
+using System.Threading;
+using System.Threading.Tasks;
+using global::DSharpPlus;
+using global::DSharpPlus.AsyncEvents;
+using global::DSharpPlus.Entities;
+using global::DSharpPlus.EventArgs;
+using global::DSharpPlus.Exceptions;
+using global::DSharpPlus.Net.Abstractions;
+using Lavalink4NET.Clients;
+using L4N = Clients.Events;
+using Lavalink4NET.Events;
+using Microsoft.Extensions.Logging;
+using System.Collections.Concurrent;
+
+///
+/// Wraps a or instance.
+///
+public sealed class DiscordClientWrapper : IDiscordClientWrapper
+{
+ ///
+ public event AsyncEventHandler? VoiceServerUpdated;
+
+ ///
+ public event AsyncEventHandler? VoiceStateUpdated;
+
+ private readonly object _client; // either DiscordShardedClient or DiscordClient
+ private readonly ILogger _logger;
+ private readonly TaskCompletionSource _readyTaskCompletionSource;
+ private bool _disposed;
+
+ private DiscordClientWrapper(object discordClient, ILogger logger)
+ {
+ ArgumentNullException.ThrowIfNull(discordClient);
+ ArgumentNullException.ThrowIfNull(logger);
+
+ _client = discordClient;
+ _logger = logger;
+
+ _readyTaskCompletionSource = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
+ }
+
+ ///
+ /// Creates a new instance of .
+ ///
+ /// The Discord Client to wrap.
+ /// a logger associated with this wrapper.
+ public DiscordClientWrapper(DiscordClient discordClient, ILogger logger)
+ : this((object)discordClient, logger)
+ {
+ ArgumentNullException.ThrowIfNull(discordClient);
+
+ void AddEventHandler(Type eventArgsType, Delegate eventHandler)
+ {
+ IClientErrorHandler errorHandler = discordClient.GetErrorHandler();
+ ConcurrentDictionary events = discordClient.GetEvents();
+
+ Type asyncEventType = typeof(AsyncEvent<,>).MakeGenericType(discordClient.GetType(), eventArgsType);
+ AsyncEvent asyncEvent = events.GetOrAdd(eventArgsType, _ => (AsyncEvent)Activator.CreateInstance
+ (
+ type: asyncEventType,
+ args: [errorHandler]
+ )!);
+
+ asyncEvent.Register(eventHandler);
+ }
+
+ AddEventHandler(typeof(VoiceStateUpdatedEventArgs), new AsyncEventHandler(OnVoiceStateUpdated));
+ AddEventHandler(typeof(VoiceServerUpdatedEventArgs), new AsyncEventHandler(OnVoiceServerUpdated));
+ AddEventHandler(typeof(GuildDownloadCompletedEventArgs), new AsyncEventHandler(OnGuildDownloadCompleted));
+ }
+
+ ///
+ /// Creates a new instance of .
+ ///
+ /// The Sharded Discord Client to wrap.
+ /// a logger associated with this wrapper.
+ public DiscordClientWrapper(DiscordShardedClient shardedDiscordClient, ILogger logger)
+ : this((object)shardedDiscordClient, logger)
+ {
+ ArgumentNullException.ThrowIfNull(shardedDiscordClient);
+
+ shardedDiscordClient.VoiceStateUpdated += OnVoiceStateUpdated;
+ shardedDiscordClient.VoiceServerUpdated += OnVoiceServerUpdated;
+ shardedDiscordClient.GuildDownloadCompleted += OnGuildDownloadCompleted;
+ }
+
+ ///
+ /// thrown if the instance is disposed
+ public async ValueTask> GetChannelUsersAsync(
+ ulong guildId,
+ ulong voiceChannelId,
+ bool includeBots = false,
+ CancellationToken cancellationToken = default)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ DiscordChannel channel;
+ try
+ {
+ channel = await GetClientForGuild(guildId)
+ .GetChannelAsync(voiceChannelId)
+ .ConfigureAwait(false);
+
+ if (channel is null)
+ {
+ return ImmutableArray.Empty;
+ }
+ }
+ catch (DiscordException exception)
+ {
+ _logger.LogWarning(
+ exception, "An error occurred while retrieving the users for voice channel '{VoiceChannelId}' of the guild '{GuildId}'.",
+ voiceChannelId, guildId);
+
+ return ImmutableArray.Empty;
+ }
+
+ var filteredUsers = ImmutableArray.CreateBuilder(channel.Users.Count);
+
+ foreach (var member in channel.Users)
+ {
+ // Always skip the current user.
+ // If we're not including bots and the member is a bot, skip them.
+ if (!member.IsCurrent || includeBots || !member.IsBot)
+ {
+ filteredUsers.Add(member.Id);
+ }
+ }
+
+ return filteredUsers.ToImmutable();
+ }
+
+ ///
+ /// thrown if the instance is disposed
+ public async ValueTask SendVoiceUpdateAsync(
+ ulong guildId,
+ ulong? voiceChannelId,
+ bool selfDeaf = false,
+ bool selfMute = false,
+ CancellationToken cancellationToken = default)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ var client = GetClientForGuild(guildId);
+
+ var payload = new VoiceStateUpdatePayload
+ {
+ GuildId = guildId,
+ ChannelId = voiceChannelId,
+ IsSelfMuted = selfMute,
+ IsSelfDeafened = selfDeaf,
+ };
+
+#pragma warning disable CS0618 // This method should not be used unless you know what you're doing. Instead, look towards the other explicitly implemented methods which come with client-side validation.
+ // Jan 23, 2024, OoLunar: We're telling Discord that we're joining a voice channel.
+ // At the time of writing, both DSharpPlus.VoiceNext and DSharpPlus.VoiceLink™
+ // use this method to send voice state updates.
+ await client
+ .SendPayloadAsync(GatewayOpCode.VoiceStateUpdate, payload)
+ .ConfigureAwait(false);
+#pragma warning restore CS0618 // This method should not be used unless you know what you're doing. Instead, look towards the other explicitly implemented methods which come with client-side validation.
+ }
+
+ ///
+ public ValueTask WaitForReadyAsync(CancellationToken cancellationToken = default)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+ return new(_readyTaskCompletionSource.Task.WaitAsync(cancellationToken));
+ }
+
+ private DiscordClient GetClientForGuild(ulong guildId) => _client is DiscordClient discordClient
+ ? discordClient
+ : ((DiscordShardedClient)_client).GetShard(guildId);
+
+ private Task OnGuildDownloadCompleted(DiscordClient discordClient, GuildDownloadCompletedEventArgs eventArgs)
+ {
+ ArgumentNullException.ThrowIfNull(discordClient);
+ ArgumentNullException.ThrowIfNull(eventArgs);
+
+ var clientInformation = new ClientInformation(
+ Label: "DSharpPlus",
+ CurrentUserId: discordClient.CurrentUser.Id,
+ ShardCount: discordClient.ShardCount);
+
+ _readyTaskCompletionSource.TrySetResult(clientInformation);
+ return Task.CompletedTask;
+ }
+
+ private async Task OnVoiceServerUpdated(DiscordClient discordClient, VoiceServerUpdatedEventArgs voiceServerUpdateEventArgs)
+ {
+ ArgumentNullException.ThrowIfNull(discordClient);
+ ArgumentNullException.ThrowIfNull(voiceServerUpdateEventArgs);
+
+ var server = new VoiceServer(
+ Token: voiceServerUpdateEventArgs.VoiceToken,
+ Endpoint: voiceServerUpdateEventArgs.Endpoint);
+
+ var eventArgs = new L4N.VoiceServerUpdatedEventArgs(
+ guildId: voiceServerUpdateEventArgs.Guild.Id,
+ voiceServer: server);
+
+ await VoiceServerUpdated
+ .InvokeAsync(this, eventArgs)
+ .ConfigureAwait(false);
+ }
+
+ private async Task OnVoiceStateUpdated(DiscordClient discordClient, VoiceStateUpdatedEventArgs voiceStateUpdateEventArgs)
+ {
+ ArgumentNullException.ThrowIfNull(discordClient);
+ ArgumentNullException.ThrowIfNull(voiceStateUpdateEventArgs);
+
+ // session id is the same as the resume key so DSharpPlus should be able to give us the
+ // session key in either before or after voice state
+ var sessionId = voiceStateUpdateEventArgs.Before?.SessionId ?? voiceStateUpdateEventArgs.After.SessionId;
+
+ // create voice state
+ var voiceState = new VoiceState(
+ VoiceChannelId: voiceStateUpdateEventArgs.After?.Channel?.Id,
+ SessionId: sessionId);
+
+ var oldVoiceState = new VoiceState(
+ VoiceChannelId: voiceStateUpdateEventArgs.Before?.Channel?.Id,
+ SessionId: sessionId);
+
+ // invoke event
+ var eventArgs = new L4N.VoiceStateUpdatedEventArgs(
+ guildId: voiceStateUpdateEventArgs.Guild.Id,
+ userId: voiceStateUpdateEventArgs.User.Id,
+ isCurrentUser: voiceStateUpdateEventArgs.User.Id == discordClient.CurrentUser.Id,
+ oldVoiceState: oldVoiceState,
+ voiceState: voiceState);
+
+ await VoiceStateUpdated
+ .InvokeAsync(this, eventArgs)
+ .ConfigureAwait(false);
+ }
+}
diff --git a/src/Lavalink4NET.DSharpPlus.Nightly/Lavalink4NET.DSharpPlus.Nightly.csproj b/src/Lavalink4NET.DSharpPlus.Nightly/Lavalink4NET.DSharpPlus.Nightly.csproj
new file mode 100644
index 00000000..c561a925
--- /dev/null
+++ b/src/Lavalink4NET.DSharpPlus.Nightly/Lavalink4NET.DSharpPlus.Nightly.csproj
@@ -0,0 +1,18 @@
+
+
+ Library
+ net8.0
+ latest
+
+ High performance Lavalink wrapper for .NET | Add powerful audio playback to your DSharpPlus-nightly-based applications with this integration for Lavalink4NET. Suitable for end users developing with DSharpPlus Nightly builds.
+ lavalink,lavalink-wrapper,discord,discord-music,discord-music-bot,dsharpplus
+
+ true
+ False
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/Lavalink4NET.DSharpPlus.Nightly/ServiceCollectionExtensions.cs b/src/Lavalink4NET.DSharpPlus.Nightly/ServiceCollectionExtensions.cs
new file mode 100644
index 00000000..f127068f
--- /dev/null
+++ b/src/Lavalink4NET.DSharpPlus.Nightly/ServiceCollectionExtensions.cs
@@ -0,0 +1,22 @@
+namespace Lavalink4NET.Extensions;
+
+using System;
+using Lavalink4NET.DSharpPlus;
+using Microsoft.Extensions.DependencyInjection;
+
+///
+/// A collection of extension methods for .
+///
+public static class ServiceCollectionExtensions
+{
+ ///
+ /// Adds the Lavalink4NET DSharpPlus extension to the service collection.
+ ///
+ /// The service collection to add the extension to.
+ /// The service collection for chaining.
+ public static IServiceCollection AddLavalink(this IServiceCollection services)
+ {
+ ArgumentNullException.ThrowIfNull(services);
+ return services.AddLavalink();
+ }
+}
diff --git a/src/Lavalink4NET.DSharpPlus.Nightly/VoiceStateUpdatePayload.cs b/src/Lavalink4NET.DSharpPlus.Nightly/VoiceStateUpdatePayload.cs
new file mode 100644
index 00000000..49e329ab
--- /dev/null
+++ b/src/Lavalink4NET.DSharpPlus.Nightly/VoiceStateUpdatePayload.cs
@@ -0,0 +1,18 @@
+namespace Lavalink4NET.DSharpPlus;
+
+using Newtonsoft.Json;
+
+internal sealed class VoiceStateUpdatePayload
+{
+ [JsonProperty("guild_id")]
+ public ulong GuildId { get; init; }
+
+ [JsonProperty("channel_id")]
+ public ulong? ChannelId { get; init; }
+
+ [JsonProperty("self_mute")]
+ public bool IsSelfMuted { get; init; }
+
+ [JsonProperty("self_deaf")]
+ public bool IsSelfDeafened { get; init; }
+}
diff --git a/src/Lavalink4NET.sln b/src/Lavalink4NET.sln
index 4aab41d1..035963e4 100644
--- a/src/Lavalink4NET.sln
+++ b/src/Lavalink4NET.sln
@@ -88,13 +88,15 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Lavalink4NET.Integrations.L
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Lavalink4NET.Integrations.Lavasrc.Tests", "..\tests\Lavalink4NET.Integrations.Lavasrc.Tests\Lavalink4NET.Integrations.Lavasrc.Tests.csproj", "{5779F765-5F0D-422C-984A-7E44EAE737C8}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lavalink4NET.NetCord", "Lavalink4NET.NetCord\Lavalink4NET.NetCord.csproj", "{8587F98B-CFE1-4559-9614-ED3B2B0C4F4E}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Lavalink4NET.NetCord", "Lavalink4NET.NetCord\Lavalink4NET.NetCord.csproj", "{8587F98B-CFE1-4559-9614-ED3B2B0C4F4E}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lavalink4NET.Samples.NetCord", "..\samples\Lavalink4NET.Samples.NetCord\Lavalink4NET.Samples.NetCord.csproj", "{02FE863F-D979-439A-9A51-C4EA69D58D29}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Lavalink4NET.Samples.NetCord", "..\samples\Lavalink4NET.Samples.NetCord\Lavalink4NET.Samples.NetCord.csproj", "{02FE863F-D979-439A-9A51-C4EA69D58D29}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lavalink4NET.Integrations.LyricsJava", "Lavalink4NET.Integrations.LyricsJava\Lavalink4NET.Integrations.LyricsJava.csproj", "{9A30E985-6D67-41D4-A12F-F1ADCD2ED0FE}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Lavalink4NET.Integrations.LyricsJava", "Lavalink4NET.Integrations.LyricsJava\Lavalink4NET.Integrations.LyricsJava.csproj", "{9A30E985-6D67-41D4-A12F-F1ADCD2ED0FE}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lavalink4NET.Integrations.LyricsJava.Tests", "Lavalink4NET.Integrations.LyricsJava.Tests\Lavalink4NET.Integrations.LyricsJava.Tests.csproj", "{176B0345-DF57-42B4-A8FD-4E6436D9554C}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Lavalink4NET.Integrations.LyricsJava.Tests", "Lavalink4NET.Integrations.LyricsJava.Tests\Lavalink4NET.Integrations.LyricsJava.Tests.csproj", "{176B0345-DF57-42B4-A8FD-4E6436D9554C}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Lavalink4NET.DSharpPlus.Nightly", "Lavalink4NET.DSharpPlus.Nightly\Lavalink4NET.DSharpPlus.Nightly.csproj", "{1A30629A-399B-4293-B5F4-B3909C1772D0}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@@ -258,6 +260,10 @@ Global
{176B0345-DF57-42B4-A8FD-4E6436D9554C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{176B0345-DF57-42B4-A8FD-4E6436D9554C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{176B0345-DF57-42B4-A8FD-4E6436D9554C}.Release|Any CPU.Build.0 = Release|Any CPU
+ {1A30629A-399B-4293-B5F4-B3909C1772D0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {1A30629A-399B-4293-B5F4-B3909C1772D0}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {1A30629A-399B-4293-B5F4-B3909C1772D0}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {1A30629A-399B-4293-B5F4-B3909C1772D0}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -286,6 +292,7 @@ Global
{02FE863F-D979-439A-9A51-C4EA69D58D29} = {B9402D29-5B12-4672-97B8-570A60C0F878}
{9A30E985-6D67-41D4-A12F-F1ADCD2ED0FE} = {48ECDC71-B9E3-4086-8194-DA81B4667CA6}
{176B0345-DF57-42B4-A8FD-4E6436D9554C} = {48ECDC71-B9E3-4086-8194-DA81B4667CA6}
+ {1A30629A-399B-4293-B5F4-B3909C1772D0} = {5FAEC63E-9752-48C4-8BC9-B101E0DBDBD3}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {466A619D-5C4B-4A8F-9852-7A5322F160A2}