From 578a2d97075f19d9481d2fc9635f5a40b7424f02 Mon Sep 17 00:00:00 2001 From: Nick Davies Date: Fri, 17 Jul 2026 15:42:53 -0600 Subject: [PATCH] Add optional group name to DeathLinkService The "DeathLink" connection tag was hardcoded in send, receive-filter, enable, and disable, so clients could not participate in Grouped DeathLink (per-group death pools). Add an optional group name, exposed via a new CreateDeathLinkService(session, group) overload, that suffixes the tag ("DeathLink" + group). A null or empty group leaves the tag exactly "DeathLink", so existing clients and the default pool are backward compatible. The existing no-arg overload and all public method signatures are untouched. --- .../DeathLinkFixture.cs | 99 +++++++++++++++++++ .../DeathLink/DeathLinkProvider.cs | 9 ++ .../DeathLink/DeathLinkService.cs | 23 +++-- 3 files changed, 122 insertions(+), 9 deletions(-) diff --git a/Archipelago.MultiClient.Net.Tests/DeathLinkFixture.cs b/Archipelago.MultiClient.Net.Tests/DeathLinkFixture.cs index 79442be1..f75c675a 100644 --- a/Archipelago.MultiClient.Net.Tests/DeathLinkFixture.cs +++ b/Archipelago.MultiClient.Net.Tests/DeathLinkFixture.cs @@ -201,5 +201,104 @@ public void Should_disable_death_link_if_its_already_enabled() Assert.That(connectionInfo.Tags.Length, Is.EqualTo(1)); Assert.That(connectionInfo.Tags[0], Is.EqualTo("SomeTag")); } + + [Test] + public void Should_enable_death_link_with_group_tag() + { + var socket = Substitute.For(); + var connectionInfo = new ConnectionInfoHelper(socket) { Tags = Array.Empty() }; + + var sut = new DeathLinkService(socket, connectionInfo, "MyGroup"); + + sut.EnableDeathLink(); + + socket.Received().SendPacket(Arg.Is(p => p.Tags.Length == 1 && p.Tags[0] == "DeathLinkMyGroup")); + + Assert.That(connectionInfo.Tags.Length, Is.EqualTo(1)); + Assert.That(connectionInfo.Tags[0], Is.EqualTo("DeathLinkMyGroup")); + } + + [Test] + public void Should_send_death_link_with_group_tag() + { + var socket = Substitute.For(); + var connectionInfo = Substitute.For(); + + var sut = new DeathLinkService(socket, connectionInfo, "MyGroup"); + + sut.SendDeathLink(new DeathLink("TestPlayer", "Died")); + + socket.Received().SendPacketAsync(Arg.Is(p => p.Tags.Count == 1 && p.Tags[0] == "DeathLinkMyGroup")); + } + + [Test] + public void Should_receive_death_link_with_matching_group_tag() + { + var socket = Substitute.For(); + var connectionInfo = Substitute.For(); + + var sut = new DeathLinkService(socket, connectionInfo, "MyGroup"); + + DeathLink receivedDeathLink = null; + sut.OnDeathLinkReceived += dl => receivedDeathLink = dl; + + var death = new DeathLink("SomeoneElse", "Died"); + var bouncePacket = new BouncedPacket { + Tags = new List { "DeathLinkMyGroup" }, + Data = new Dictionary { + { "source", death.Source }, + { "time", death.Timestamp.ToUnixTimeStamp() }, + { "cause", death.Cause } + } + }; + + socket.PacketReceived += Raise.Event(bouncePacket); + + Assert.That(receivedDeathLink, Is.Not.Null); + } + + [Test] + public void Should_not_receive_death_link_from_a_different_group() + { + var socket = Substitute.For(); + var connectionInfo = Substitute.For(); + + var sut = new DeathLinkService(socket, connectionInfo, "MyGroup"); + + DeathLink receivedDeathLink = null; + sut.OnDeathLinkReceived += dl => receivedDeathLink = dl; + + // A fully-valid death in the default pool ("DeathLink") must not reach a grouped client. + var death = new DeathLink("SomeoneElse", "Died"); + var bouncePacket = new BouncedPacket { + Tags = new List { "DeathLink" }, + Data = new Dictionary { + { "source", death.Source }, + { "time", death.Timestamp.ToUnixTimeStamp() }, + { "cause", death.Cause } + } + }; + + socket.PacketReceived += Raise.Event(bouncePacket); + + Assert.That(receivedDeathLink, Is.Null); + } + + [Test] + public void Should_use_default_tag_for_blank_group() + { + var socket = Substitute.For(); + var connectionInfo = new ConnectionInfoHelper(socket) { Tags = Array.Empty() }; + + // Blank group is wire-identical to standard DeathLink (default pool). + var sut = new DeathLinkService(socket, connectionInfo, ""); + + sut.EnableDeathLink(); + + socket.Received().SendPacket(Arg.Is(p => p.Tags.Length == 1 && p.Tags[0] == "DeathLink")); + + Assert.That(connectionInfo.Tags.Length, Is.EqualTo(1)); + Assert.That(connectionInfo.Tags[0], Is.EqualTo("DeathLink")); + } } } diff --git a/Archipelago.MultiClient.Net/BounceFeatures/DeathLink/DeathLinkProvider.cs b/Archipelago.MultiClient.Net/BounceFeatures/DeathLink/DeathLinkProvider.cs index 76d80218..b8e76c24 100644 --- a/Archipelago.MultiClient.Net/BounceFeatures/DeathLink/DeathLinkProvider.cs +++ b/Archipelago.MultiClient.Net/BounceFeatures/DeathLink/DeathLinkProvider.cs @@ -9,5 +9,14 @@ public static class DeathLinkProvider /// public static DeathLinkService CreateDeathLinkService(this ArchipelagoSession session) => new DeathLinkService(session.Socket, session.ConnectionInfo); + + // ReSharper disable once UnusedMember.Global + /// + /// creates and returns a for this , + /// scoped to the DeathLink . Only players sharing the same group name + /// send and receive deaths to one another; a null or empty group joins the default DeathLink pool. + /// + public static DeathLinkService CreateDeathLinkService(this ArchipelagoSession session, string group) => + new DeathLinkService(session.Socket, session.ConnectionInfo, group); } } \ No newline at end of file diff --git a/Archipelago.MultiClient.Net/BounceFeatures/DeathLink/DeathLinkService.cs b/Archipelago.MultiClient.Net/BounceFeatures/DeathLink/DeathLinkService.cs index c9bedd10..54dad607 100644 --- a/Archipelago.MultiClient.Net/BounceFeatures/DeathLink/DeathLinkService.cs +++ b/Archipelago.MultiClient.Net/BounceFeatures/DeathLink/DeathLinkService.cs @@ -12,6 +12,7 @@ public class DeathLinkService { readonly IArchipelagoSocketHelper socket; readonly IConnectionInfoProvider connectionInfoProvider; + readonly string tag; DeathLink lastSendDeathLink; @@ -26,10 +27,12 @@ public class DeathLinkService /// public event DeathLinkReceivedHandler OnDeathLinkReceived; - internal DeathLinkService(IArchipelagoSocketHelper socket, IConnectionInfoProvider connectionInfoProvider) + internal DeathLinkService(IArchipelagoSocketHelper socket, IConnectionInfoProvider connectionInfoProvider, + string group = null) { this.socket = socket; this.connectionInfoProvider = connectionInfoProvider; + this.tag = "DeathLink" + (group ?? ""); socket.PacketReceived += OnPacketReceived; } @@ -38,7 +41,7 @@ void OnPacketReceived(ArchipelagoPacketBase packet) { switch (packet) { - case BouncedPacket bouncedPacket when bouncedPacket.Tags.Contains("DeathLink"): + case BouncedPacket bouncedPacket when bouncedPacket.Tags.Contains(tag): if (DeathLink.TryParse(bouncedPacket.Data, out var deathLink)) { if (lastSendDeathLink != null && lastSendDeathLink == deathLink) @@ -66,7 +69,7 @@ public void SendDeathLink(DeathLink deathLink) { var bouncePacket = new BouncePacket { - Tags = new List { "DeathLink" }, + Tags = new List { tag }, Data = new Dictionary { {"time", deathLink.Timestamp.ToUnixTimeStamp()}, {"source", deathLink.Source}, @@ -82,27 +85,29 @@ public void SendDeathLink(DeathLink deathLink) } /// - /// Adds "DeathLink" to your 's tags and opts you in to receiving + /// Adds the DeathLink tag (suffixed with the group name, if any) to your + /// 's tags and opts you in to receiving /// events /// public void EnableDeathLink() { - if (Array.IndexOf(connectionInfoProvider.Tags, "DeathLink") == -1) + if (Array.IndexOf(connectionInfoProvider.Tags, tag) == -1) connectionInfoProvider.UpdateConnectionOptions( - connectionInfoProvider.Tags.Concat(new[] { "DeathLink" }).ToArray()); + connectionInfoProvider.Tags.Concat(new[] { tag }).ToArray()); } /// - /// Removes the "DeathLink" tag from your and opts out of further + /// Removes the DeathLink tag (suffixed with the group name, if any) from your + /// and opts out of further /// events /// public void DisableDeathLink() { - if (Array.IndexOf(connectionInfoProvider.Tags, "DeathLink") == -1) + if (Array.IndexOf(connectionInfoProvider.Tags, tag) == -1) return; connectionInfoProvider.UpdateConnectionOptions( - connectionInfoProvider.Tags.Where(t => t != "DeathLink").ToArray()); + connectionInfoProvider.Tags.Where(t => t != tag).ToArray()); } } }