From 36e51ba3898527bc6de32975036dd6cd18df67e6 Mon Sep 17 00:00:00 2001 From: Derrick Timmermans Date: Fri, 27 Aug 2021 22:03:41 +0200 Subject: [PATCH 01/37] Initial attempts at IPC --- .../IO/TestSceneGameplayEventBroadcaster.cs | 100 ++++++++++++++++++ .../IO/GameplayEventBroadcaster.cs | 79 ++++++++++++++ 2 files changed, 179 insertions(+) create mode 100644 osu.Game.Rulesets.Sentakki.Tests/IO/TestSceneGameplayEventBroadcaster.cs create mode 100644 osu.Game.Rulesets.Sentakki/IO/GameplayEventBroadcaster.cs diff --git a/osu.Game.Rulesets.Sentakki.Tests/IO/TestSceneGameplayEventBroadcaster.cs b/osu.Game.Rulesets.Sentakki.Tests/IO/TestSceneGameplayEventBroadcaster.cs new file mode 100644 index 000000000..0985f0ce2 --- /dev/null +++ b/osu.Game.Rulesets.Sentakki.Tests/IO/TestSceneGameplayEventBroadcaster.cs @@ -0,0 +1,100 @@ +using System; +using System.IO.Pipes; +using System.Security.Principal; +using System.Text; +using System.Threading; +using osu.Framework.Graphics.Sprites; +using osu.Game.Rulesets.Sentakki.IO; +using osu.Game.Tests.Visual; + +namespace osu.Game.Rulesets.Sentakki.Tests +{ + public class TestSceneGameplayEventBroadcaster : OsuTestScene + { + private GameplayEventBroadcaster broadcaster; + private TestBroadcastClient client; + private SpriteText text; + + public TestSceneGameplayEventBroadcaster() + { + Add(text = new SpriteText() + { + Text = "Nothing here yet" + }); + + AddStep("Start broadcaster", () => broadcaster = new GameplayEventBroadcaster()); + AddStep("Send message", () => broadcaster.Broadcast("Testing1")); + AddStep("CreateClient", () => client = new TestBroadcastClient(text)); + AddStep("Send message 1", () => broadcaster.Broadcast("Testing1")); + AddAssert("Client received message 1", () => text.Text == "Testing1"); + AddStep("Send message 2", () => broadcaster.Broadcast("Testing2")); + AddAssert("Client received message 2", () => text.Text == "Testing2"); + AddStep("Send message 3", () => broadcaster.Broadcast("Testing3")); + AddAssert("Client received message 3", () => text.Text == "Testing3"); + AddStep("Kill client", () => { client?.Dispose(); client = null; }); + AddStep("Kill broadcaster", () => broadcaster?.Dispose()); + } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + client?.Dispose(); + broadcaster?.Dispose(); + } + + private class TestBroadcastClient : IDisposable + { + private NamedPipeClientStream pipeServer; + + private SpriteText text; + + private bool running = true; + + private Thread readThread; + + public TestBroadcastClient(SpriteText outputText) + { + text = outputText; + pipeServer = new NamedPipeClientStream(".", "senPipe", + PipeDirection.In, PipeOptions.Asynchronous, + TokenImpersonationLevel.Impersonation); + + readThread = new Thread(clientLoop); + readThread.Start(); + } + + private void clientLoop() + { + while (running) + { + if (!pipeServer.IsConnected) + pipeServer.Connect(); + try + { + var len = pipeServer.ReadByte(); + if (len > 0) + { + var buffer = new byte[len]; + + pipeServer.Read(buffer, 0, len); + text.Text = Encoding.UTF8.GetString(buffer); + } + } + catch + { + pipeServer.Close(); + } + } + pipeServer.Close(); + pipeServer.Dispose(); + } + + public void Dispose() + { + running = false; + pipeServer.Close(); + pipeServer.Dispose(); + } + } + } +} diff --git a/osu.Game.Rulesets.Sentakki/IO/GameplayEventBroadcaster.cs b/osu.Game.Rulesets.Sentakki/IO/GameplayEventBroadcaster.cs new file mode 100644 index 000000000..d5db7c64c --- /dev/null +++ b/osu.Game.Rulesets.Sentakki/IO/GameplayEventBroadcaster.cs @@ -0,0 +1,79 @@ +using System; +using System.IO.Pipes; +using System.Text; + +namespace osu.Game.Rulesets.Sentakki.IO +{ + public class GameplayEventBroadcaster : IDisposable + { + private NamedPipeServerStream pipeServer; + + public GameplayEventBroadcaster() + { + pipeServer = new NamedPipeServerStream("senPipe", PipeDirection.InOut, 1, PipeTransmissionMode.Byte, PipeOptions.Asynchronous); + onClientDisconnected(); + } + + private bool isWaitingForClient; + + private void onClientDisconnected() + { + if (isWaitingForClient) return; + + isWaitingForClient = true; + pipeServer.BeginWaitForConnection(new AsyncCallback(waitForConnectionCallBack), this); + } + + private void waitForConnectionCallBack(IAsyncResult result) + { + try + { + pipeServer.EndWaitForConnection(result); + isWaitingForClient = false; + } + catch + { + // If the pipe is closed before a client ever connects, + // EndWaitForConnection() will throw an exception. + + // If we are in here that is probably the case so just return. + return; + } + } + + public void Broadcast(string message) + { + if (!connectionValid()) return; + + var buffer = Encoding.UTF8.GetBytes(message); + var len = (byte)buffer.Length; + pipeServer.WriteByte(len); + pipeServer.Write(buffer, 0, len); + } + + private bool connectionValid() + { + if (isWaitingForClient) return false; + + try { pipeServer.WriteByte(0); } + catch + { + pipeServer.Disconnect(); + onClientDisconnected(); + + return false; + } + + // We assume that connection is valid at this point + return true; + } + + public void Dispose() + { + pipeServer.Disconnect(); + pipeServer.Dispose(); + + isWaitingForClient = true; + } + } +} From ed99cab63bf79e80d3a52b4396423d534ac7efe7 Mon Sep 17 00:00:00 2001 From: Derrick Timmermans Date: Fri, 10 Sep 2021 12:54:53 +0200 Subject: [PATCH 02/37] Create a transmission protocol to be used by the broadcaster --- .../IO/TestSceneGameplayEventBroadcaster.cs | 2 +- .../IO/TestSceneTransmissionProtocol.cs | 29 ++++++++++++ .../IO/TransmissionProtocol.cs | 44 +++++++++++++++++++ 3 files changed, 74 insertions(+), 1 deletion(-) create mode 100644 osu.Game.Rulesets.Sentakki.Tests/IO/TestSceneTransmissionProtocol.cs create mode 100644 osu.Game.Rulesets.Sentakki/IO/TransmissionProtocol.cs diff --git a/osu.Game.Rulesets.Sentakki.Tests/IO/TestSceneGameplayEventBroadcaster.cs b/osu.Game.Rulesets.Sentakki.Tests/IO/TestSceneGameplayEventBroadcaster.cs index 0985f0ce2..79ccb4c5b 100644 --- a/osu.Game.Rulesets.Sentakki.Tests/IO/TestSceneGameplayEventBroadcaster.cs +++ b/osu.Game.Rulesets.Sentakki.Tests/IO/TestSceneGameplayEventBroadcaster.cs @@ -7,7 +7,7 @@ using osu.Game.Rulesets.Sentakki.IO; using osu.Game.Tests.Visual; -namespace osu.Game.Rulesets.Sentakki.Tests +namespace osu.Game.Rulesets.Sentakki.Tests.IO { public class TestSceneGameplayEventBroadcaster : OsuTestScene { diff --git a/osu.Game.Rulesets.Sentakki.Tests/IO/TestSceneTransmissionProtocol.cs b/osu.Game.Rulesets.Sentakki.Tests/IO/TestSceneTransmissionProtocol.cs new file mode 100644 index 000000000..fb24d182a --- /dev/null +++ b/osu.Game.Rulesets.Sentakki.Tests/IO/TestSceneTransmissionProtocol.cs @@ -0,0 +1,29 @@ +using NUnit.Framework; +using osu.Framework.Testing; +using osu.Game.Rulesets.Sentakki.IO; + +namespace osu.Game.Rulesets.Sentakki.Tests.IO +{ + [HeadlessTest] + public class TestSceneTransmissionProtocol + { + private const TransimssionData.InfoType testtype = TransimssionData.InfoType.Miss; + private const int testvalue = 7; + private const byte testbyte = ((byte)testtype << 3) + 7; + + [Test] + public void TestEncode() + { + var encoded = new TransimssionData(testtype, testvalue); + Assert.AreEqual(testbyte, encoded.RawData); + } + + [Test] + public void TestDecode() + { + var encoded = new TransimssionData(testbyte); + Assert.AreEqual(testtype, encoded.Type); + Assert.AreEqual(testvalue, encoded.Value); + } + } +} diff --git a/osu.Game.Rulesets.Sentakki/IO/TransmissionProtocol.cs b/osu.Game.Rulesets.Sentakki/IO/TransmissionProtocol.cs new file mode 100644 index 000000000..79f39930a --- /dev/null +++ b/osu.Game.Rulesets.Sentakki/IO/TransmissionProtocol.cs @@ -0,0 +1,44 @@ +namespace osu.Game.Rulesets.Sentakki.IO +{ + public struct TransimssionData + { + public enum InfoType : byte + { + MetaStartPlay, + MetaEndPlay, + + HitPerfect, + HitGreat, + HitGood, + Miss, + } + + public TransimssionData(byte input) + { + RawData = input; + + int value = 0; + for (int i = 0; i < 3; ++i) + { + value += (input & 1) << i; + input >>= 1; + } + Value = value; + Type = (InfoType)input; + } + + public TransimssionData(InfoType type, int value) + { + RawData = (byte)((int)type << 3); + RawData += (byte)(value); + Type = type; + Value = value; + } + + public byte RawData; + + public InfoType Type; + + public int Value; + } +} From fa840b47da9b642f0b2159415622f9f66e6f016d Mon Sep 17 00:00:00 2001 From: Derrick Timmermans Date: Fri, 10 Sep 2021 14:57:26 +0200 Subject: [PATCH 03/37] Use created protocol in broadcaster --- .../IO/TestSceneGameplayEventBroadcaster.cs | 24 ++++++++----------- .../IO/GameplayEventBroadcaster.cs | 7 ++---- .../IO/TransmissionProtocol.cs | 3 +++ 3 files changed, 15 insertions(+), 19 deletions(-) diff --git a/osu.Game.Rulesets.Sentakki.Tests/IO/TestSceneGameplayEventBroadcaster.cs b/osu.Game.Rulesets.Sentakki.Tests/IO/TestSceneGameplayEventBroadcaster.cs index 79ccb4c5b..37d2b1c68 100644 --- a/osu.Game.Rulesets.Sentakki.Tests/IO/TestSceneGameplayEventBroadcaster.cs +++ b/osu.Game.Rulesets.Sentakki.Tests/IO/TestSceneGameplayEventBroadcaster.cs @@ -1,7 +1,6 @@ using System; using System.IO.Pipes; using System.Security.Principal; -using System.Text; using System.Threading; using osu.Framework.Graphics.Sprites; using osu.Game.Rulesets.Sentakki.IO; @@ -23,14 +22,14 @@ public TestSceneGameplayEventBroadcaster() }); AddStep("Start broadcaster", () => broadcaster = new GameplayEventBroadcaster()); - AddStep("Send message", () => broadcaster.Broadcast("Testing1")); + AddStep("Send message", () => broadcaster.Broadcast(new TransimssionData(TransimssionData.InfoType.MetaStartPlay, 3))); AddStep("CreateClient", () => client = new TestBroadcastClient(text)); - AddStep("Send message 1", () => broadcaster.Broadcast("Testing1")); - AddAssert("Client received message 1", () => text.Text == "Testing1"); - AddStep("Send message 2", () => broadcaster.Broadcast("Testing2")); - AddAssert("Client received message 2", () => text.Text == "Testing2"); - AddStep("Send message 3", () => broadcaster.Broadcast("Testing3")); - AddAssert("Client received message 3", () => text.Text == "Testing3"); + AddStep("Send message 1", () => broadcaster.Broadcast(new TransimssionData(TransimssionData.InfoType.HitPerfect, 3))); + AddUntilStep("Client received message 1", () => text.Text == new TransimssionData(TransimssionData.InfoType.HitPerfect, 3).ToString()); + AddStep("Send message 2", () => broadcaster.Broadcast(new TransimssionData(TransimssionData.InfoType.MetaEndPlay, 3))); + AddUntilStep("Client received message 2", () => text.Text == new TransimssionData(TransimssionData.InfoType.MetaEndPlay, 3).ToString()); + AddStep("Send message 3", () => broadcaster.Broadcast(new TransimssionData(TransimssionData.InfoType.Miss, 3))); + AddUntilStep("Client received message 3", () => text.Text == new TransimssionData(TransimssionData.InfoType.Miss, 3).ToString()); AddStep("Kill client", () => { client?.Dispose(); client = null; }); AddStep("Kill broadcaster", () => broadcaster?.Dispose()); } @@ -71,13 +70,10 @@ private void clientLoop() pipeServer.Connect(); try { - var len = pipeServer.ReadByte(); - if (len > 0) + byte packet = (byte)pipeServer.ReadByte(); + if (packet > 0) { - var buffer = new byte[len]; - - pipeServer.Read(buffer, 0, len); - text.Text = Encoding.UTF8.GetString(buffer); + text.Text = new TransimssionData(packet).ToString(); } } catch diff --git a/osu.Game.Rulesets.Sentakki/IO/GameplayEventBroadcaster.cs b/osu.Game.Rulesets.Sentakki/IO/GameplayEventBroadcaster.cs index d5db7c64c..f1efc12df 100644 --- a/osu.Game.Rulesets.Sentakki/IO/GameplayEventBroadcaster.cs +++ b/osu.Game.Rulesets.Sentakki/IO/GameplayEventBroadcaster.cs @@ -41,14 +41,11 @@ private void waitForConnectionCallBack(IAsyncResult result) } } - public void Broadcast(string message) + public void Broadcast(TransimssionData packet) { if (!connectionValid()) return; - var buffer = Encoding.UTF8.GetBytes(message); - var len = (byte)buffer.Length; - pipeServer.WriteByte(len); - pipeServer.Write(buffer, 0, len); + pipeServer.WriteByte(packet.RawData); } private bool connectionValid() diff --git a/osu.Game.Rulesets.Sentakki/IO/TransmissionProtocol.cs b/osu.Game.Rulesets.Sentakki/IO/TransmissionProtocol.cs index 79f39930a..b90d51db8 100644 --- a/osu.Game.Rulesets.Sentakki/IO/TransmissionProtocol.cs +++ b/osu.Game.Rulesets.Sentakki/IO/TransmissionProtocol.cs @@ -4,6 +4,7 @@ public struct TransimssionData { public enum InfoType : byte { + None, MetaStartPlay, MetaEndPlay, @@ -40,5 +41,7 @@ public TransimssionData(InfoType type, int value) public InfoType Type; public int Value; + + public override string ToString() => Type.ToString() + " " + Value; } } From 6328b158edc15b8c4c615394302ad03bcfb66431 Mon Sep 17 00:00:00 2001 From: Derrick Timmermans Date: Fri, 10 Sep 2021 15:03:19 +0200 Subject: [PATCH 04/37] Wait for client connection in test --- .../IO/TestSceneGameplayEventBroadcaster.cs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Sentakki.Tests/IO/TestSceneGameplayEventBroadcaster.cs b/osu.Game.Rulesets.Sentakki.Tests/IO/TestSceneGameplayEventBroadcaster.cs index 37d2b1c68..8e7a82b71 100644 --- a/osu.Game.Rulesets.Sentakki.Tests/IO/TestSceneGameplayEventBroadcaster.cs +++ b/osu.Game.Rulesets.Sentakki.Tests/IO/TestSceneGameplayEventBroadcaster.cs @@ -23,13 +23,14 @@ public TestSceneGameplayEventBroadcaster() AddStep("Start broadcaster", () => broadcaster = new GameplayEventBroadcaster()); AddStep("Send message", () => broadcaster.Broadcast(new TransimssionData(TransimssionData.InfoType.MetaStartPlay, 3))); - AddStep("CreateClient", () => client = new TestBroadcastClient(text)); + AddStep("Create Client", () => client = new TestBroadcastClient(text)); + AddUntilStep("Client connected", () => client.IsClientConnected); AddStep("Send message 1", () => broadcaster.Broadcast(new TransimssionData(TransimssionData.InfoType.HitPerfect, 3))); - AddUntilStep("Client received message 1", () => text.Text == new TransimssionData(TransimssionData.InfoType.HitPerfect, 3).ToString()); + AddAssert("Client received message 1", () => text.Text == new TransimssionData(TransimssionData.InfoType.HitPerfect, 3).ToString()); AddStep("Send message 2", () => broadcaster.Broadcast(new TransimssionData(TransimssionData.InfoType.MetaEndPlay, 3))); - AddUntilStep("Client received message 2", () => text.Text == new TransimssionData(TransimssionData.InfoType.MetaEndPlay, 3).ToString()); + AddAssert("Client received message 2", () => text.Text == new TransimssionData(TransimssionData.InfoType.MetaEndPlay, 3).ToString()); AddStep("Send message 3", () => broadcaster.Broadcast(new TransimssionData(TransimssionData.InfoType.Miss, 3))); - AddUntilStep("Client received message 3", () => text.Text == new TransimssionData(TransimssionData.InfoType.Miss, 3).ToString()); + AddAssert("Client received message 3", () => text.Text == new TransimssionData(TransimssionData.InfoType.Miss, 3).ToString()); AddStep("Kill client", () => { client?.Dispose(); client = null; }); AddStep("Kill broadcaster", () => broadcaster?.Dispose()); } @@ -49,6 +50,8 @@ private class TestBroadcastClient : IDisposable private bool running = true; + public bool IsClientConnected => pipeServer.IsConnected; + private Thread readThread; public TestBroadcastClient(SpriteText outputText) From 614b86b1055c18b29e031b44cb2d48cd19bbf244 Mon Sep 17 00:00:00 2001 From: Derrick Timmermans Date: Fri, 10 Sep 2021 17:35:11 +0200 Subject: [PATCH 05/37] Fix typo in TransmissionData --- .../IO/TestSceneGameplayEventBroadcaster.cs | 16 ++++++++-------- .../IO/TestSceneTransmissionProtocol.cs | 6 +++--- .../IO/GameplayEventBroadcaster.cs | 2 +- .../IO/TransmissionProtocol.cs | 6 +++--- 4 files changed, 15 insertions(+), 15 deletions(-) diff --git a/osu.Game.Rulesets.Sentakki.Tests/IO/TestSceneGameplayEventBroadcaster.cs b/osu.Game.Rulesets.Sentakki.Tests/IO/TestSceneGameplayEventBroadcaster.cs index 8e7a82b71..4e8cf6b95 100644 --- a/osu.Game.Rulesets.Sentakki.Tests/IO/TestSceneGameplayEventBroadcaster.cs +++ b/osu.Game.Rulesets.Sentakki.Tests/IO/TestSceneGameplayEventBroadcaster.cs @@ -22,15 +22,15 @@ public TestSceneGameplayEventBroadcaster() }); AddStep("Start broadcaster", () => broadcaster = new GameplayEventBroadcaster()); - AddStep("Send message", () => broadcaster.Broadcast(new TransimssionData(TransimssionData.InfoType.MetaStartPlay, 3))); + AddStep("Send message", () => broadcaster.Broadcast(new TransmissionData(TransmissionData.InfoType.MetaStartPlay, 3))); AddStep("Create Client", () => client = new TestBroadcastClient(text)); AddUntilStep("Client connected", () => client.IsClientConnected); - AddStep("Send message 1", () => broadcaster.Broadcast(new TransimssionData(TransimssionData.InfoType.HitPerfect, 3))); - AddAssert("Client received message 1", () => text.Text == new TransimssionData(TransimssionData.InfoType.HitPerfect, 3).ToString()); - AddStep("Send message 2", () => broadcaster.Broadcast(new TransimssionData(TransimssionData.InfoType.MetaEndPlay, 3))); - AddAssert("Client received message 2", () => text.Text == new TransimssionData(TransimssionData.InfoType.MetaEndPlay, 3).ToString()); - AddStep("Send message 3", () => broadcaster.Broadcast(new TransimssionData(TransimssionData.InfoType.Miss, 3))); - AddAssert("Client received message 3", () => text.Text == new TransimssionData(TransimssionData.InfoType.Miss, 3).ToString()); + AddStep("Send message 1", () => broadcaster.Broadcast(new TransmissionData(TransmissionData.InfoType.HitPerfect, 3))); + AddAssert("Client received message 1", () => text.Text == new TransmissionData(TransmissionData.InfoType.HitPerfect, 3).ToString()); + AddStep("Send message 2", () => broadcaster.Broadcast(new TransmissionData(TransmissionData.InfoType.MetaEndPlay, 3))); + AddAssert("Client received message 2", () => text.Text == new TransmissionData(TransmissionData.InfoType.MetaEndPlay, 3).ToString()); + AddStep("Send message 3", () => broadcaster.Broadcast(new TransmissionData(TransmissionData.InfoType.Miss, 3))); + AddAssert("Client received message 3", () => text.Text == new TransmissionData(TransmissionData.InfoType.Miss, 3).ToString()); AddStep("Kill client", () => { client?.Dispose(); client = null; }); AddStep("Kill broadcaster", () => broadcaster?.Dispose()); } @@ -76,7 +76,7 @@ private void clientLoop() byte packet = (byte)pipeServer.ReadByte(); if (packet > 0) { - text.Text = new TransimssionData(packet).ToString(); + text.Text = new TransmissionData(packet).ToString(); } } catch diff --git a/osu.Game.Rulesets.Sentakki.Tests/IO/TestSceneTransmissionProtocol.cs b/osu.Game.Rulesets.Sentakki.Tests/IO/TestSceneTransmissionProtocol.cs index fb24d182a..92e5b4c2d 100644 --- a/osu.Game.Rulesets.Sentakki.Tests/IO/TestSceneTransmissionProtocol.cs +++ b/osu.Game.Rulesets.Sentakki.Tests/IO/TestSceneTransmissionProtocol.cs @@ -7,21 +7,21 @@ namespace osu.Game.Rulesets.Sentakki.Tests.IO [HeadlessTest] public class TestSceneTransmissionProtocol { - private const TransimssionData.InfoType testtype = TransimssionData.InfoType.Miss; + private const TransmissionData.InfoType testtype = TransmissionData.InfoType.Miss; private const int testvalue = 7; private const byte testbyte = ((byte)testtype << 3) + 7; [Test] public void TestEncode() { - var encoded = new TransimssionData(testtype, testvalue); + var encoded = new TransmissionData(testtype, testvalue); Assert.AreEqual(testbyte, encoded.RawData); } [Test] public void TestDecode() { - var encoded = new TransimssionData(testbyte); + var encoded = new TransmissionData(testbyte); Assert.AreEqual(testtype, encoded.Type); Assert.AreEqual(testvalue, encoded.Value); } diff --git a/osu.Game.Rulesets.Sentakki/IO/GameplayEventBroadcaster.cs b/osu.Game.Rulesets.Sentakki/IO/GameplayEventBroadcaster.cs index f1efc12df..0a94f4246 100644 --- a/osu.Game.Rulesets.Sentakki/IO/GameplayEventBroadcaster.cs +++ b/osu.Game.Rulesets.Sentakki/IO/GameplayEventBroadcaster.cs @@ -41,7 +41,7 @@ private void waitForConnectionCallBack(IAsyncResult result) } } - public void Broadcast(TransimssionData packet) + public void Broadcast(TransmissionData packet) { if (!connectionValid()) return; diff --git a/osu.Game.Rulesets.Sentakki/IO/TransmissionProtocol.cs b/osu.Game.Rulesets.Sentakki/IO/TransmissionProtocol.cs index b90d51db8..65a522a28 100644 --- a/osu.Game.Rulesets.Sentakki/IO/TransmissionProtocol.cs +++ b/osu.Game.Rulesets.Sentakki/IO/TransmissionProtocol.cs @@ -1,6 +1,6 @@ namespace osu.Game.Rulesets.Sentakki.IO { - public struct TransimssionData + public struct TransmissionData { public enum InfoType : byte { @@ -14,7 +14,7 @@ public enum InfoType : byte Miss, } - public TransimssionData(byte input) + public TransmissionData(byte input) { RawData = input; @@ -28,7 +28,7 @@ public TransimssionData(byte input) Type = (InfoType)input; } - public TransimssionData(InfoType type, int value) + public TransmissionData(InfoType type, int value) { RawData = (byte)((int)type << 3); RawData += (byte)(value); From 32073a058c2ad5c3257fd77719f92e2eefca9dc7 Mon Sep 17 00:00:00 2001 From: Derrick Timmermans Date: Sun, 12 Sep 2021 21:13:08 +0200 Subject: [PATCH 06/37] Make protocol equatable --- .../IO/TransmissionProtocol.cs | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Sentakki/IO/TransmissionProtocol.cs b/osu.Game.Rulesets.Sentakki/IO/TransmissionProtocol.cs index 65a522a28..4d4c39339 100644 --- a/osu.Game.Rulesets.Sentakki/IO/TransmissionProtocol.cs +++ b/osu.Game.Rulesets.Sentakki/IO/TransmissionProtocol.cs @@ -1,7 +1,11 @@ +using System; + namespace osu.Game.Rulesets.Sentakki.IO { - public struct TransmissionData + public struct TransmissionData : IEquatable { + public static TransmissionData Empty => new TransmissionData(0); + public enum InfoType : byte { None, @@ -43,5 +47,15 @@ public TransmissionData(InfoType type, int value) public int Value; public override string ToString() => Type.ToString() + " " + Value; + + + public override bool Equals(object obj) => obj is TransmissionData other && Equals(other); + + public bool Equals(TransmissionData other) => Type == other.Type && Value == other.Value; + + public override int GetHashCode() => (Type, Value).GetHashCode(); + + public static bool operator ==(TransmissionData a, TransmissionData b) => a.Equals(b); + public static bool operator !=(TransmissionData a, TransmissionData b) => !a.Equals(b); } } From 4f116ffa0ca3e59cad5a8a2979f03297c86afd57 Mon Sep 17 00:00:00 2001 From: Derrick Timmermans Date: Sun, 12 Sep 2021 21:15:24 +0200 Subject: [PATCH 07/37] Store data into a buffer before use Allows a message to be resent on reconnect --- .../IO/GameplayEventBroadcaster.cs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/osu.Game.Rulesets.Sentakki/IO/GameplayEventBroadcaster.cs b/osu.Game.Rulesets.Sentakki/IO/GameplayEventBroadcaster.cs index 0a94f4246..04959e4e0 100644 --- a/osu.Game.Rulesets.Sentakki/IO/GameplayEventBroadcaster.cs +++ b/osu.Game.Rulesets.Sentakki/IO/GameplayEventBroadcaster.cs @@ -8,6 +8,10 @@ public class GameplayEventBroadcaster : IDisposable { private NamedPipeServerStream pipeServer; + // This is used to store the message that needs to be sent + // In the event that a broadcast fails, we can resend this message once a new connection is established. + private TransmissionData queuedData; + public GameplayEventBroadcaster() { pipeServer = new NamedPipeServerStream("senPipe", PipeDirection.InOut, 1, PipeTransmissionMode.Byte, PipeOptions.Asynchronous); @@ -30,6 +34,9 @@ private void waitForConnectionCallBack(IAsyncResult result) { pipeServer.EndWaitForConnection(result); isWaitingForClient = false; + + if (queuedData != TransmissionData.Empty) + Broadcast(queuedData); } catch { @@ -43,9 +50,11 @@ private void waitForConnectionCallBack(IAsyncResult result) public void Broadcast(TransmissionData packet) { + queuedData = packet; if (!connectionValid()) return; pipeServer.WriteByte(packet.RawData); + queuedData = TransmissionData.Empty; } private bool connectionValid() From 7d178e10a8cf540ad331b9dd32fc7f1497c2e73c Mon Sep 17 00:00:00 2001 From: Derrick Timmermans Date: Sun, 12 Sep 2021 21:25:27 +0200 Subject: [PATCH 08/37] Minor cleaning --- .../IO/TestSceneGameplayEventBroadcaster.cs | 9 --------- .../IO/GameplayEventBroadcaster.cs | 10 +--------- 2 files changed, 1 insertion(+), 18 deletions(-) diff --git a/osu.Game.Rulesets.Sentakki.Tests/IO/TestSceneGameplayEventBroadcaster.cs b/osu.Game.Rulesets.Sentakki.Tests/IO/TestSceneGameplayEventBroadcaster.cs index 4e8cf6b95..db65d1e63 100644 --- a/osu.Game.Rulesets.Sentakki.Tests/IO/TestSceneGameplayEventBroadcaster.cs +++ b/osu.Game.Rulesets.Sentakki.Tests/IO/TestSceneGameplayEventBroadcaster.cs @@ -31,15 +31,6 @@ public TestSceneGameplayEventBroadcaster() AddAssert("Client received message 2", () => text.Text == new TransmissionData(TransmissionData.InfoType.MetaEndPlay, 3).ToString()); AddStep("Send message 3", () => broadcaster.Broadcast(new TransmissionData(TransmissionData.InfoType.Miss, 3))); AddAssert("Client received message 3", () => text.Text == new TransmissionData(TransmissionData.InfoType.Miss, 3).ToString()); - AddStep("Kill client", () => { client?.Dispose(); client = null; }); - AddStep("Kill broadcaster", () => broadcaster?.Dispose()); - } - - protected override void Dispose(bool isDisposing) - { - base.Dispose(isDisposing); - client?.Dispose(); - broadcaster?.Dispose(); } private class TestBroadcastClient : IDisposable diff --git a/osu.Game.Rulesets.Sentakki/IO/GameplayEventBroadcaster.cs b/osu.Game.Rulesets.Sentakki/IO/GameplayEventBroadcaster.cs index 04959e4e0..a77e40b88 100644 --- a/osu.Game.Rulesets.Sentakki/IO/GameplayEventBroadcaster.cs +++ b/osu.Game.Rulesets.Sentakki/IO/GameplayEventBroadcaster.cs @@ -4,7 +4,7 @@ namespace osu.Game.Rulesets.Sentakki.IO { - public class GameplayEventBroadcaster : IDisposable + public class GameplayEventBroadcaster { private NamedPipeServerStream pipeServer; @@ -73,13 +73,5 @@ private bool connectionValid() // We assume that connection is valid at this point return true; } - - public void Dispose() - { - pipeServer.Disconnect(); - pipeServer.Dispose(); - - isWaitingForClient = true; - } } } From 8a2332d200ef4ffc2798e0f24f647abbd5b7713b Mon Sep 17 00:00:00 2001 From: Derrick Timmermans Date: Mon, 13 Sep 2021 08:58:57 +0200 Subject: [PATCH 09/37] Make broadcaster disposable --- osu.Game.Rulesets.Sentakki/IO/GameplayEventBroadcaster.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Sentakki/IO/GameplayEventBroadcaster.cs b/osu.Game.Rulesets.Sentakki/IO/GameplayEventBroadcaster.cs index a77e40b88..ef7beb9ce 100644 --- a/osu.Game.Rulesets.Sentakki/IO/GameplayEventBroadcaster.cs +++ b/osu.Game.Rulesets.Sentakki/IO/GameplayEventBroadcaster.cs @@ -4,7 +4,7 @@ namespace osu.Game.Rulesets.Sentakki.IO { - public class GameplayEventBroadcaster + public class GameplayEventBroadcaster : IDisposable { private NamedPipeServerStream pipeServer; @@ -73,5 +73,10 @@ private bool connectionValid() // We assume that connection is valid at this point return true; } + + public void Dispose() + { + pipeServer.Dispose(); + } } } From c080f600487051ec626998a3ec879da2ca31be37 Mon Sep 17 00:00:00 2001 From: Derrick Timmermans Date: Mon, 13 Sep 2021 09:17:20 +0200 Subject: [PATCH 10/37] Integrate broadcaster into gameplay --- .../IO/TransmissionProtocol.cs | 2 ++ .../UI/DrawableSentakkiRuleset.cs | 18 +++++++++++++ osu.Game.Rulesets.Sentakki/UI/Lane.cs | 7 ++++++ .../UI/LanedPlayfield.cs | 25 +++++++++++++++++++ .../UI/SentakkiPlayfield.cs | 5 ++++ 5 files changed, 57 insertions(+) diff --git a/osu.Game.Rulesets.Sentakki/IO/TransmissionProtocol.cs b/osu.Game.Rulesets.Sentakki/IO/TransmissionProtocol.cs index 4d4c39339..fe26995f6 100644 --- a/osu.Game.Rulesets.Sentakki/IO/TransmissionProtocol.cs +++ b/osu.Game.Rulesets.Sentakki/IO/TransmissionProtocol.cs @@ -16,6 +16,8 @@ public enum InfoType : byte HitGreat, HitGood, Miss, + + LanePressed, } public TransmissionData(byte input) diff --git a/osu.Game.Rulesets.Sentakki/UI/DrawableSentakkiRuleset.cs b/osu.Game.Rulesets.Sentakki/UI/DrawableSentakkiRuleset.cs index ba79e6445..8973a0371 100644 --- a/osu.Game.Rulesets.Sentakki/UI/DrawableSentakkiRuleset.cs +++ b/osu.Game.Rulesets.Sentakki/UI/DrawableSentakkiRuleset.cs @@ -9,6 +9,7 @@ using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Sentakki.Configuration; +using osu.Game.Rulesets.Sentakki.IO; using osu.Game.Rulesets.Sentakki.Objects; using osu.Game.Rulesets.Sentakki.Replays; using osu.Game.Rulesets.UI; @@ -32,6 +33,17 @@ protected override void LoadComplete() (Config as SentakkiRulesetConfigManager)?.BindWith(SentakkiRulesetSettings.LaneInputMode, laneInputMode); } + private GameplayEventBroadcaster eventBroadcaster; + + protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) + { + var dependencies = new DependencyContainer(base.CreateChildDependencies(parent)); + + dependencies.CacheAs(eventBroadcaster = new GameplayEventBroadcaster()); + + return dependencies; + } + // Input specifics (sensor/button) for replay and gameplay private readonly Bindable laneInputMode = new Bindable(); @@ -60,5 +72,11 @@ protected override void LoadComplete() protected override ResumeOverlay CreateResumeOverlay() => new SentakkiResumeOverlay(); protected override Framework.Input.PassThroughInputManager CreateInputManager() => new SentakkiInputManager(Ruleset?.RulesetInfo); + + protected override void Dispose(bool isDisposing) + { + eventBroadcaster.Dispose(); + base.Dispose(isDisposing); + } } } diff --git a/osu.Game.Rulesets.Sentakki/UI/Lane.cs b/osu.Game.Rulesets.Sentakki/UI/Lane.cs index f135515b6..00f3a9412 100644 --- a/osu.Game.Rulesets.Sentakki/UI/Lane.cs +++ b/osu.Game.Rulesets.Sentakki/UI/Lane.cs @@ -8,6 +8,7 @@ using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Sentakki.Configuration; +using osu.Game.Rulesets.Sentakki.IO; using osu.Game.Rulesets.Sentakki.Objects; using osu.Game.Rulesets.Sentakki.Objects.Drawables; using osu.Game.Rulesets.UI; @@ -17,6 +18,9 @@ namespace osu.Game.Rulesets.Sentakki.UI { public class Lane : Playfield, IKeyBindingHandler { + [Resolved] + private GameplayEventBroadcaster eventBroadcaster { get; set; } + public int LaneNumber { get; set; } public Action OnLoaded; @@ -116,7 +120,10 @@ private void handleKeyPress(ValueChangedEvent keys) SentakkiActionInputManager.TriggerReleased(SentakkiAction.Key1 + LaneNumber); if (keys.NewValue > keys.OldValue) + { SentakkiActionInputManager.TriggerPressed(SentakkiAction.Key1 + LaneNumber); + eventBroadcaster.Broadcast(new TransmissionData(TransmissionData.InfoType.LanePressed, LaneNumber)); + } } public bool OnPressed(SentakkiAction action) diff --git a/osu.Game.Rulesets.Sentakki/UI/LanedPlayfield.cs b/osu.Game.Rulesets.Sentakki/UI/LanedPlayfield.cs index 85cfc1280..c4b2c1860 100644 --- a/osu.Game.Rulesets.Sentakki/UI/LanedPlayfield.cs +++ b/osu.Game.Rulesets.Sentakki/UI/LanedPlayfield.cs @@ -6,6 +6,8 @@ using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Drawables; +using osu.Game.Rulesets.Scoring; +using osu.Game.Rulesets.Sentakki.IO; using osu.Game.Rulesets.Sentakki.Objects; using osu.Game.Rulesets.Sentakki.Objects.Drawables; using osu.Game.Rulesets.Sentakki.Objects.Drawables.Pieces; @@ -17,6 +19,10 @@ namespace osu.Game.Rulesets.Sentakki.UI { public class LanedPlayfield : Playfield { + + [Resolved] + private GameplayEventBroadcaster eventBroadcaster { get; set; } + public readonly List Lanes = new List(); private readonly SortedDrawableProxyContainer slideBodyProxyContainer; @@ -117,6 +123,25 @@ private void onNewResult(DrawableHitObject judgedObject, JudgementResult result) if (judgedObject is DrawableSlideBody) return; + TransmissionData.InfoType resultType = TransmissionData.InfoType.None; + switch (result.Type) + { + case HitResult.Great: + resultType = TransmissionData.InfoType.HitPerfect; + break; + case HitResult.Good: + resultType = TransmissionData.InfoType.HitGreat; + break; + case HitResult.Meh: + resultType = TransmissionData.InfoType.HitGood; + break; + case HitResult.Miss: + resultType = TransmissionData.InfoType.Miss; + break; + } + + eventBroadcaster.Broadcast(new TransmissionData(resultType, laned.HitObject.Lane)); + var explosion = explosionPool.Get(e => e.Apply(laned.HitObject)); explosionLayer.Add(explosion); } diff --git a/osu.Game.Rulesets.Sentakki/UI/SentakkiPlayfield.cs b/osu.Game.Rulesets.Sentakki/UI/SentakkiPlayfield.cs index 26f9c3808..d4e6ce099 100644 --- a/osu.Game.Rulesets.Sentakki/UI/SentakkiPlayfield.cs +++ b/osu.Game.Rulesets.Sentakki/UI/SentakkiPlayfield.cs @@ -9,6 +9,7 @@ using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Sentakki.Configuration; +using osu.Game.Rulesets.Sentakki.IO; using osu.Game.Rulesets.Sentakki.Objects; using osu.Game.Rulesets.Sentakki.Objects.Drawables; using osu.Game.Rulesets.Sentakki.UI.Components; @@ -22,6 +23,9 @@ namespace osu.Game.Rulesets.Sentakki.UI [Cached] public class SentakkiPlayfield : Playfield { + [Resolved] + private GameplayEventBroadcaster gameplayEventBroadcaster { get; set; } + private readonly Container judgementLayer; private readonly DrawablePool judgementPool; @@ -112,6 +116,7 @@ protected override void LoadComplete() skin.BindValueChanged(_ => changePlayfieldAccent(), true); ringColor.BindValueChanged(_ => changePlayfieldAccent(), true); + gameplayEventBroadcaster.Broadcast(new TransmissionData(TransmissionData.InfoType.MetaStartPlay, 0)); } protected override HitObjectLifetimeEntry CreateLifetimeEntry(HitObject hitObject) => new SentakkiHitObjectLifetimeEntry(hitObject, sentakkiRulesetConfig, drawableSentakkiRuleset); From 4df36d93a708ce6d0676a9bbfbf449f825c9047f Mon Sep 17 00:00:00 2001 From: Derrick Timmermans Date: Mon, 13 Sep 2021 09:20:53 +0200 Subject: [PATCH 11/37] Make things readonly --- .../IO/TestSceneGameplayEventBroadcaster.cs | 3 +-- osu.Game.Rulesets.Sentakki/IO/GameplayEventBroadcaster.cs | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Sentakki.Tests/IO/TestSceneGameplayEventBroadcaster.cs b/osu.Game.Rulesets.Sentakki.Tests/IO/TestSceneGameplayEventBroadcaster.cs index db65d1e63..d245465f9 100644 --- a/osu.Game.Rulesets.Sentakki.Tests/IO/TestSceneGameplayEventBroadcaster.cs +++ b/osu.Game.Rulesets.Sentakki.Tests/IO/TestSceneGameplayEventBroadcaster.cs @@ -12,7 +12,7 @@ public class TestSceneGameplayEventBroadcaster : OsuTestScene { private GameplayEventBroadcaster broadcaster; private TestBroadcastClient client; - private SpriteText text; + private readonly SpriteText text; public TestSceneGameplayEventBroadcaster() { @@ -82,7 +82,6 @@ private void clientLoop() public void Dispose() { running = false; - pipeServer.Close(); pipeServer.Dispose(); } } diff --git a/osu.Game.Rulesets.Sentakki/IO/GameplayEventBroadcaster.cs b/osu.Game.Rulesets.Sentakki/IO/GameplayEventBroadcaster.cs index ef7beb9ce..69b29ef0d 100644 --- a/osu.Game.Rulesets.Sentakki/IO/GameplayEventBroadcaster.cs +++ b/osu.Game.Rulesets.Sentakki/IO/GameplayEventBroadcaster.cs @@ -1,12 +1,11 @@ using System; using System.IO.Pipes; -using System.Text; namespace osu.Game.Rulesets.Sentakki.IO { public class GameplayEventBroadcaster : IDisposable { - private NamedPipeServerStream pipeServer; + private readonly NamedPipeServerStream pipeServer; // This is used to store the message that needs to be sent // In the event that a broadcast fails, we can resend this message once a new connection is established. From 0fe99a09c20a2b5cf82ae7f44dc8830b7cb11a66 Mon Sep 17 00:00:00 2001 From: Derrick Timmermans Date: Tue, 21 Sep 2021 09:22:48 +0200 Subject: [PATCH 12/37] Add Kill InfoType --- .../IO/TestSceneGameplayEventBroadcaster.cs | 1 + .../IO/TestSceneTransmissionProtocol.cs | 9 +++++++++ osu.Game.Rulesets.Sentakki/IO/TransmissionProtocol.cs | 6 +++++- 3 files changed, 15 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Sentakki.Tests/IO/TestSceneGameplayEventBroadcaster.cs b/osu.Game.Rulesets.Sentakki.Tests/IO/TestSceneGameplayEventBroadcaster.cs index d245465f9..1513d8c9d 100644 --- a/osu.Game.Rulesets.Sentakki.Tests/IO/TestSceneGameplayEventBroadcaster.cs +++ b/osu.Game.Rulesets.Sentakki.Tests/IO/TestSceneGameplayEventBroadcaster.cs @@ -31,6 +31,7 @@ public TestSceneGameplayEventBroadcaster() AddAssert("Client received message 2", () => text.Text == new TransmissionData(TransmissionData.InfoType.MetaEndPlay, 3).ToString()); AddStep("Send message 3", () => broadcaster.Broadcast(new TransmissionData(TransmissionData.InfoType.Miss, 3))); AddAssert("Client received message 3", () => text.Text == new TransmissionData(TransmissionData.InfoType.Miss, 3).ToString()); + AddStep("Dispose broadcaster", () => broadcaster.Dispose()); } private class TestBroadcastClient : IDisposable diff --git a/osu.Game.Rulesets.Sentakki.Tests/IO/TestSceneTransmissionProtocol.cs b/osu.Game.Rulesets.Sentakki.Tests/IO/TestSceneTransmissionProtocol.cs index 92e5b4c2d..4fe64e3d1 100644 --- a/osu.Game.Rulesets.Sentakki.Tests/IO/TestSceneTransmissionProtocol.cs +++ b/osu.Game.Rulesets.Sentakki.Tests/IO/TestSceneTransmissionProtocol.cs @@ -25,5 +25,14 @@ public void TestDecode() Assert.AreEqual(testtype, encoded.Type); Assert.AreEqual(testvalue, encoded.Value); } + + [Test] + public void TestKillEquality() + { + var packet1 = new TransmissionData(TransmissionData.InfoType.Kill, 7); // Baseline + var packet2 = new TransmissionData(TransmissionData.InfoType.Kill, 0); // Lightly altered, but should still be considered the same, because of the kill flag + + Assert.AreEqual(packet1, packet2); + } } } diff --git a/osu.Game.Rulesets.Sentakki/IO/TransmissionProtocol.cs b/osu.Game.Rulesets.Sentakki/IO/TransmissionProtocol.cs index fe26995f6..9f7f6980b 100644 --- a/osu.Game.Rulesets.Sentakki/IO/TransmissionProtocol.cs +++ b/osu.Game.Rulesets.Sentakki/IO/TransmissionProtocol.cs @@ -18,6 +18,10 @@ public enum InfoType : byte Miss, LanePressed, + + // an 0xFF byte is sent when broadcaster is killed + // After shifting the bits right 3 times, we get 31 + Kill = 31, } public TransmissionData(byte input) @@ -53,7 +57,7 @@ public TransmissionData(InfoType type, int value) public override bool Equals(object obj) => obj is TransmissionData other && Equals(other); - public bool Equals(TransmissionData other) => Type == other.Type && Value == other.Value; + public bool Equals(TransmissionData other) => (Type == InfoType.Kill && other.Type == InfoType.Kill) || (Type == other.Type && Value == other.Value); public override int GetHashCode() => (Type, Value).GetHashCode(); From 436a927798ccd9096d19c44a5bbe830f078337a4 Mon Sep 17 00:00:00 2001 From: Derrick Timmermans Date: Tue, 21 Sep 2021 14:56:12 +0200 Subject: [PATCH 13/37] Use immediately form a TransmissionData --- .../IO/TestSceneGameplayEventBroadcaster.cs | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/osu.Game.Rulesets.Sentakki.Tests/IO/TestSceneGameplayEventBroadcaster.cs b/osu.Game.Rulesets.Sentakki.Tests/IO/TestSceneGameplayEventBroadcaster.cs index 1513d8c9d..4801b77e3 100644 --- a/osu.Game.Rulesets.Sentakki.Tests/IO/TestSceneGameplayEventBroadcaster.cs +++ b/osu.Game.Rulesets.Sentakki.Tests/IO/TestSceneGameplayEventBroadcaster.cs @@ -63,13 +63,12 @@ private void clientLoop() { if (!pipeServer.IsConnected) pipeServer.Connect(); + try { - byte packet = (byte)pipeServer.ReadByte(); - if (packet > 0) - { - text.Text = new TransmissionData(packet).ToString(); - } + TransmissionData packet = new TransmissionData((byte)pipeServer.ReadByte()); + if (packet != TransmissionData.Empty) + text.Text = packet.ToString(); } catch { From 40f26ba4c3d7cce470426e23168559640b5d2d81 Mon Sep 17 00:00:00 2001 From: Derrick Timmermans Date: Tue, 21 Sep 2021 15:10:36 +0200 Subject: [PATCH 14/37] Add a kill static --- osu.Game.Rulesets.Sentakki/IO/TransmissionProtocol.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game.Rulesets.Sentakki/IO/TransmissionProtocol.cs b/osu.Game.Rulesets.Sentakki/IO/TransmissionProtocol.cs index 9f7f6980b..5ad52cc6e 100644 --- a/osu.Game.Rulesets.Sentakki/IO/TransmissionProtocol.cs +++ b/osu.Game.Rulesets.Sentakki/IO/TransmissionProtocol.cs @@ -6,6 +6,8 @@ public struct TransmissionData : IEquatable { public static TransmissionData Empty => new TransmissionData(0); + public static TransmissionData Kill => new TransmissionData(InfoType.Kill, 7); + public enum InfoType : byte { None, From a19f35e33eba94a0fb943f0e16295252a9c420bb Mon Sep 17 00:00:00 2001 From: Derrick Timmermans Date: Tue, 21 Sep 2021 15:15:26 +0200 Subject: [PATCH 15/37] Ensure that test read thread doesn't stay alive --- .../IO/TestSceneGameplayEventBroadcaster.cs | 39 +++++++++++++------ 1 file changed, 27 insertions(+), 12 deletions(-) diff --git a/osu.Game.Rulesets.Sentakki.Tests/IO/TestSceneGameplayEventBroadcaster.cs b/osu.Game.Rulesets.Sentakki.Tests/IO/TestSceneGameplayEventBroadcaster.cs index 4801b77e3..0b2a715e3 100644 --- a/osu.Game.Rulesets.Sentakki.Tests/IO/TestSceneGameplayEventBroadcaster.cs +++ b/osu.Game.Rulesets.Sentakki.Tests/IO/TestSceneGameplayEventBroadcaster.cs @@ -34,6 +34,12 @@ public TestSceneGameplayEventBroadcaster() AddStep("Dispose broadcaster", () => broadcaster.Dispose()); } + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + client?.Dispose(); + } + private class TestBroadcastClient : IDisposable { private NamedPipeClientStream pipeServer; @@ -57,31 +63,40 @@ public TestBroadcastClient(SpriteText outputText) readThread.Start(); } + private CancellationTokenSource cancellationTokenSource = new CancellationTokenSource(); + private CancellationToken cancellationToken => cancellationTokenSource.Token; + private void clientLoop() { while (running) { if (!pipeServer.IsConnected) - pipeServer.Connect(); - - try { - TransmissionData packet = new TransmissionData((byte)pipeServer.ReadByte()); - if (packet != TransmissionData.Empty) - text.Text = packet.ToString(); - } - catch - { - pipeServer.Close(); + try + { + pipeServer.ConnectAsync().Wait(cancellationToken); + } + catch (OperationCanceledException) + { + break; + } } + + TransmissionData packet = new TransmissionData((byte)pipeServer.ReadByte()); + + // Server has shut down + if (packet == TransmissionData.Kill) + continue; + + if (packet != TransmissionData.Empty) + text.Text = packet.ToString(); } - pipeServer.Close(); - pipeServer.Dispose(); } public void Dispose() { running = false; + cancellationTokenSource.Cancel(); pipeServer.Dispose(); } } From a6967d1867f8973b49b5444d8b3df0fe3b582274 Mon Sep 17 00:00:00 2001 From: Derrick Timmermans Date: Tue, 21 Sep 2021 20:48:33 +0200 Subject: [PATCH 16/37] Fire lane press event before anything else --- osu.Game.Rulesets.Sentakki/UI/Lane.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Sentakki/UI/Lane.cs b/osu.Game.Rulesets.Sentakki/UI/Lane.cs index 3d14efd36..87277c7be 100644 --- a/osu.Game.Rulesets.Sentakki/UI/Lane.cs +++ b/osu.Game.Rulesets.Sentakki/UI/Lane.cs @@ -122,8 +122,8 @@ private void handleKeyPress(ValueChangedEvent keys) if (keys.NewValue > keys.OldValue) { - SentakkiActionInputManager.TriggerPressed(SentakkiAction.Key1 + LaneNumber); eventBroadcaster.Broadcast(new TransmissionData(TransmissionData.InfoType.LanePressed, LaneNumber)); + SentakkiActionInputManager.TriggerPressed(SentakkiAction.Key1 + LaneNumber); } } From eaa8cd578f7dae1875a5b17787992c7bb4753f4d Mon Sep 17 00:00:00 2001 From: Derrick Timmermans Date: Tue, 21 Sep 2021 20:51:12 +0200 Subject: [PATCH 17/37] Disallow attempts to use disposed broadcaster --- osu.Game.Rulesets.Sentakki/IO/GameplayEventBroadcaster.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/osu.Game.Rulesets.Sentakki/IO/GameplayEventBroadcaster.cs b/osu.Game.Rulesets.Sentakki/IO/GameplayEventBroadcaster.cs index 69b29ef0d..95432ab8a 100644 --- a/osu.Game.Rulesets.Sentakki/IO/GameplayEventBroadcaster.cs +++ b/osu.Game.Rulesets.Sentakki/IO/GameplayEventBroadcaster.cs @@ -11,6 +11,8 @@ public class GameplayEventBroadcaster : IDisposable // In the event that a broadcast fails, we can resend this message once a new connection is established. private TransmissionData queuedData; + private bool isDisposed; + public GameplayEventBroadcaster() { pipeServer = new NamedPipeServerStream("senPipe", PipeDirection.InOut, 1, PipeTransmissionMode.Byte, PipeOptions.Asynchronous); @@ -49,6 +51,9 @@ private void waitForConnectionCallBack(IAsyncResult result) public void Broadcast(TransmissionData packet) { + if (isDisposed) + throw new ObjectDisposedException(nameof(GameplayEventBroadcaster)); + queuedData = packet; if (!connectionValid()) return; @@ -75,6 +80,7 @@ private bool connectionValid() public void Dispose() { + isDisposed = true; pipeServer.Dispose(); } } From 0cb36a24d5752b3a887517e8be7fd57e8d1271a6 Mon Sep 17 00:00:00 2001 From: Derrick Timmermans Date: Tue, 21 Sep 2021 21:06:08 +0200 Subject: [PATCH 18/37] Some cleanups --- .../IO/TestSceneGameplayEventBroadcaster.cs | 9 +++++---- .../IO/GameplayEventBroadcaster.cs | 18 +++++++++--------- 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/osu.Game.Rulesets.Sentakki.Tests/IO/TestSceneGameplayEventBroadcaster.cs b/osu.Game.Rulesets.Sentakki.Tests/IO/TestSceneGameplayEventBroadcaster.cs index 0b2a715e3..e372e50cb 100644 --- a/osu.Game.Rulesets.Sentakki.Tests/IO/TestSceneGameplayEventBroadcaster.cs +++ b/osu.Game.Rulesets.Sentakki.Tests/IO/TestSceneGameplayEventBroadcaster.cs @@ -32,6 +32,7 @@ public TestSceneGameplayEventBroadcaster() AddStep("Send message 3", () => broadcaster.Broadcast(new TransmissionData(TransmissionData.InfoType.Miss, 3))); AddAssert("Client received message 3", () => text.Text == new TransmissionData(TransmissionData.InfoType.Miss, 3).ToString()); AddStep("Dispose broadcaster", () => broadcaster.Dispose()); + AddStep("Dispose client", () => client?.Dispose()); } protected override void Dispose(bool isDisposing) @@ -42,15 +43,15 @@ protected override void Dispose(bool isDisposing) private class TestBroadcastClient : IDisposable { - private NamedPipeClientStream pipeServer; + private readonly NamedPipeClientStream pipeServer; - private SpriteText text; + private readonly SpriteText text; private bool running = true; public bool IsClientConnected => pipeServer.IsConnected; - private Thread readThread; + private readonly Thread readThread; public TestBroadcastClient(SpriteText outputText) { @@ -63,7 +64,7 @@ public TestBroadcastClient(SpriteText outputText) readThread.Start(); } - private CancellationTokenSource cancellationTokenSource = new CancellationTokenSource(); + private readonly CancellationTokenSource cancellationTokenSource = new CancellationTokenSource(); private CancellationToken cancellationToken => cancellationTokenSource.Token; private void clientLoop() diff --git a/osu.Game.Rulesets.Sentakki/IO/GameplayEventBroadcaster.cs b/osu.Game.Rulesets.Sentakki/IO/GameplayEventBroadcaster.cs index 95432ab8a..b6ccdbe72 100644 --- a/osu.Game.Rulesets.Sentakki/IO/GameplayEventBroadcaster.cs +++ b/osu.Game.Rulesets.Sentakki/IO/GameplayEventBroadcaster.cs @@ -1,4 +1,5 @@ using System; +using System.IO; using System.IO.Pipes; namespace osu.Game.Rulesets.Sentakki.IO @@ -35,18 +36,16 @@ private void waitForConnectionCallBack(IAsyncResult result) { pipeServer.EndWaitForConnection(result); isWaitingForClient = false; - - if (queuedData != TransmissionData.Empty) - Broadcast(queuedData); } - catch + catch (IOException) { - // If the pipe is closed before a client ever connects, - // EndWaitForConnection() will throw an exception. - - // If we are in here that is probably the case so just return. + // The server has been disposed, abort wait return; } + + if (queuedData != TransmissionData.Empty) + Broadcast(queuedData); + } public void Broadcast(TransmissionData packet) @@ -66,8 +65,9 @@ private bool connectionValid() if (isWaitingForClient) return false; try { pipeServer.WriteByte(0); } - catch + catch (IOException) { + // The client has suddenly disconnected, we must disconnect on our end, and wait for a new connection. pipeServer.Disconnect(); onClientDisconnected(); From 5b8c17ca8e261f11238e92b045d96a475f50f02d Mon Sep 17 00:00:00 2001 From: Derrick Timmermans Date: Tue, 21 Sep 2021 21:31:14 +0200 Subject: [PATCH 19/37] Properly name the client --- .../IO/TestSceneGameplayEventBroadcaster.cs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/osu.Game.Rulesets.Sentakki.Tests/IO/TestSceneGameplayEventBroadcaster.cs b/osu.Game.Rulesets.Sentakki.Tests/IO/TestSceneGameplayEventBroadcaster.cs index e372e50cb..544511644 100644 --- a/osu.Game.Rulesets.Sentakki.Tests/IO/TestSceneGameplayEventBroadcaster.cs +++ b/osu.Game.Rulesets.Sentakki.Tests/IO/TestSceneGameplayEventBroadcaster.cs @@ -43,20 +43,20 @@ protected override void Dispose(bool isDisposing) private class TestBroadcastClient : IDisposable { - private readonly NamedPipeClientStream pipeServer; + private readonly NamedPipeClientStream pipeClient; private readonly SpriteText text; private bool running = true; - public bool IsClientConnected => pipeServer.IsConnected; + public bool IsClientConnected => pipeClient.IsConnected; private readonly Thread readThread; public TestBroadcastClient(SpriteText outputText) { text = outputText; - pipeServer = new NamedPipeClientStream(".", "senPipe", + pipeClient = new NamedPipeClientStream(".", "senPipe", PipeDirection.In, PipeOptions.Asynchronous, TokenImpersonationLevel.Impersonation); @@ -71,11 +71,11 @@ private void clientLoop() { while (running) { - if (!pipeServer.IsConnected) + if (!pipeClient.IsConnected) { try { - pipeServer.ConnectAsync().Wait(cancellationToken); + pipeClient.ConnectAsync().Wait(cancellationToken); } catch (OperationCanceledException) { @@ -83,7 +83,7 @@ private void clientLoop() } } - TransmissionData packet = new TransmissionData((byte)pipeServer.ReadByte()); + TransmissionData packet = new TransmissionData((byte)pipeClient.ReadByte()); // Server has shut down if (packet == TransmissionData.Kill) @@ -98,7 +98,7 @@ public void Dispose() { running = false; cancellationTokenSource.Cancel(); - pipeServer.Dispose(); + pipeClient.Dispose(); } } } From 5a96b2a3dbc10550411531fe4250213cb860d0a3 Mon Sep 17 00:00:00 2001 From: Derrick Timmermans Date: Fri, 24 Sep 2021 21:01:14 +0200 Subject: [PATCH 20/37] Fix tests not working on non-windows --- .../IO/TestSceneGameplayEventBroadcaster.cs | 34 +++++++++---------- .../IO/GameplayEventBroadcaster.cs | 2 +- 2 files changed, 17 insertions(+), 19 deletions(-) diff --git a/osu.Game.Rulesets.Sentakki.Tests/IO/TestSceneGameplayEventBroadcaster.cs b/osu.Game.Rulesets.Sentakki.Tests/IO/TestSceneGameplayEventBroadcaster.cs index 544511644..c6ee7d700 100644 --- a/osu.Game.Rulesets.Sentakki.Tests/IO/TestSceneGameplayEventBroadcaster.cs +++ b/osu.Game.Rulesets.Sentakki.Tests/IO/TestSceneGameplayEventBroadcaster.cs @@ -26,11 +26,11 @@ public TestSceneGameplayEventBroadcaster() AddStep("Create Client", () => client = new TestBroadcastClient(text)); AddUntilStep("Client connected", () => client.IsClientConnected); AddStep("Send message 1", () => broadcaster.Broadcast(new TransmissionData(TransmissionData.InfoType.HitPerfect, 3))); - AddAssert("Client received message 1", () => text.Text == new TransmissionData(TransmissionData.InfoType.HitPerfect, 3).ToString()); + AddUntilStep("Client received message 1", () => text.Text == new TransmissionData(TransmissionData.InfoType.HitPerfect, 3).ToString()); AddStep("Send message 2", () => broadcaster.Broadcast(new TransmissionData(TransmissionData.InfoType.MetaEndPlay, 3))); - AddAssert("Client received message 2", () => text.Text == new TransmissionData(TransmissionData.InfoType.MetaEndPlay, 3).ToString()); + AddUntilStep("Client received message 2", () => text.Text == new TransmissionData(TransmissionData.InfoType.MetaEndPlay, 3).ToString()); AddStep("Send message 3", () => broadcaster.Broadcast(new TransmissionData(TransmissionData.InfoType.Miss, 3))); - AddAssert("Client received message 3", () => text.Text == new TransmissionData(TransmissionData.InfoType.Miss, 3).ToString()); + AddUntilStep("Client received message 3", () => text.Text == new TransmissionData(TransmissionData.InfoType.Miss, 3).ToString()); AddStep("Dispose broadcaster", () => broadcaster.Dispose()); AddStep("Dispose client", () => client?.Dispose()); } @@ -71,26 +71,24 @@ private void clientLoop() { while (running) { - if (!pipeClient.IsConnected) + try { - try - { + if (!pipeClient.IsConnected) pipeClient.ConnectAsync().Wait(cancellationToken); - } - catch (OperationCanceledException) - { - break; - } - } - TransmissionData packet = new TransmissionData((byte)pipeClient.ReadByte()); + TransmissionData packet = new TransmissionData((byte)pipeClient.ReadByte()); - // Server has shut down - if (packet == TransmissionData.Kill) - continue; + // Server has shut down + if (packet == TransmissionData.Kill) + continue; - if (packet != TransmissionData.Empty) - text.Text = packet.ToString(); + if (packet != TransmissionData.Empty) + text.Text = packet.ToString(); + } + catch (OperationCanceledException) + { + break; + } } } diff --git a/osu.Game.Rulesets.Sentakki/IO/GameplayEventBroadcaster.cs b/osu.Game.Rulesets.Sentakki/IO/GameplayEventBroadcaster.cs index b6ccdbe72..81d86e405 100644 --- a/osu.Game.Rulesets.Sentakki/IO/GameplayEventBroadcaster.cs +++ b/osu.Game.Rulesets.Sentakki/IO/GameplayEventBroadcaster.cs @@ -37,7 +37,7 @@ private void waitForConnectionCallBack(IAsyncResult result) pipeServer.EndWaitForConnection(result); isWaitingForClient = false; } - catch (IOException) + catch { // The server has been disposed, abort wait return; From a113d3f641032f3129c96866e745f123f4683437 Mon Sep 17 00:00:00 2001 From: Derrick Timmermans Date: Fri, 24 Sep 2021 21:23:20 +0200 Subject: [PATCH 21/37] More comprehensive testing --- .../IO/TestSceneGameplayEventBroadcaster.cs | 51 ++++++++++++++++++- 1 file changed, 49 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Sentakki.Tests/IO/TestSceneGameplayEventBroadcaster.cs b/osu.Game.Rulesets.Sentakki.Tests/IO/TestSceneGameplayEventBroadcaster.cs index c6ee7d700..995862602 100644 --- a/osu.Game.Rulesets.Sentakki.Tests/IO/TestSceneGameplayEventBroadcaster.cs +++ b/osu.Game.Rulesets.Sentakki.Tests/IO/TestSceneGameplayEventBroadcaster.cs @@ -2,6 +2,7 @@ using System.IO.Pipes; using System.Security.Principal; using System.Threading; +using NUnit.Framework; using osu.Framework.Graphics.Sprites; using osu.Game.Rulesets.Sentakki.IO; using osu.Game.Tests.Visual; @@ -20,9 +21,20 @@ public TestSceneGameplayEventBroadcaster() { Text = "Nothing here yet" }); + } + + [SetUp] + public void SetUpClient() + { + // Dispose existing broadcaster and client + client?.Dispose(); + broadcaster?.Dispose(); + } + [Test] + public void TestNormalOperation() + { AddStep("Start broadcaster", () => broadcaster = new GameplayEventBroadcaster()); - AddStep("Send message", () => broadcaster.Broadcast(new TransmissionData(TransmissionData.InfoType.MetaStartPlay, 3))); AddStep("Create Client", () => client = new TestBroadcastClient(text)); AddUntilStep("Client connected", () => client.IsClientConnected); AddStep("Send message 1", () => broadcaster.Broadcast(new TransmissionData(TransmissionData.InfoType.HitPerfect, 3))); @@ -31,8 +43,43 @@ public TestSceneGameplayEventBroadcaster() AddUntilStep("Client received message 2", () => text.Text == new TransmissionData(TransmissionData.InfoType.MetaEndPlay, 3).ToString()); AddStep("Send message 3", () => broadcaster.Broadcast(new TransmissionData(TransmissionData.InfoType.Miss, 3))); AddUntilStep("Client received message 3", () => text.Text == new TransmissionData(TransmissionData.InfoType.Miss, 3).ToString()); + AddStep("Client disconnect", () => client?.Dispose()); + AddStep("Dispose broadcaster", () => broadcaster.Dispose()); + } + + [Test] + public void TestOperationWithoutClient() + { + AddStep("Start broadcaster", () => broadcaster = new GameplayEventBroadcaster()); + AddStep("Send message 1", () => broadcaster.Broadcast(new TransmissionData(TransmissionData.InfoType.HitPerfect, 3))); + AddStep("Dispose broadcaster", () => broadcaster.Dispose()); + } + + [Test] + public void TestOperationWithClientDisconnect() + { + AddStep("Start broadcaster", () => broadcaster = new GameplayEventBroadcaster()); + AddStep("Create Client", () => client = new TestBroadcastClient(text)); + AddUntilStep("Client connected", () => client.IsClientConnected); + AddStep("Client disconnect", () => client?.Dispose()); + AddStep("Send message 1", () => broadcaster.Broadcast(new TransmissionData(TransmissionData.InfoType.HitPerfect, 3))); + AddStep("Dispose broadcaster", () => broadcaster.Dispose()); + } + + // This is just to ensure my sample client implementation holds up + // So others can be confident they aren't getting a sample that doesn't work + [Test] + public void TestClientOperationWithServerReconnect() + { + AddStep("Start broadcaster", () => broadcaster = new GameplayEventBroadcaster()); + AddStep("Create Client", () => client = new TestBroadcastClient(text)); + AddUntilStep("Client connected", () => client.IsClientConnected); + AddStep("Dispose broadcaster", () => broadcaster.Dispose()); + AddStep("Start new broadcaster", () => broadcaster = new GameplayEventBroadcaster()); + AddUntilStep("Client connected", () => client.IsClientConnected); + AddStep("Send message 1", () => broadcaster.Broadcast(new TransmissionData(TransmissionData.InfoType.HitPerfect, 3))); + AddUntilStep("Client received message 1", () => text.Text == new TransmissionData(TransmissionData.InfoType.HitPerfect, 3).ToString()); AddStep("Dispose broadcaster", () => broadcaster.Dispose()); - AddStep("Dispose client", () => client?.Dispose()); } protected override void Dispose(bool isDisposing) From bfbf72872c3ecf4625a4a354b864bba08f158b38 Mon Sep 17 00:00:00 2001 From: Derrick Timmermans Date: Fri, 24 Sep 2021 21:52:18 +0200 Subject: [PATCH 22/37] Dispose test broadcaster properly --- .../IO/TestSceneGameplayEventBroadcaster.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game.Rulesets.Sentakki.Tests/IO/TestSceneGameplayEventBroadcaster.cs b/osu.Game.Rulesets.Sentakki.Tests/IO/TestSceneGameplayEventBroadcaster.cs index 995862602..cdf6042b1 100644 --- a/osu.Game.Rulesets.Sentakki.Tests/IO/TestSceneGameplayEventBroadcaster.cs +++ b/osu.Game.Rulesets.Sentakki.Tests/IO/TestSceneGameplayEventBroadcaster.cs @@ -86,6 +86,7 @@ protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); client?.Dispose(); + broadcaster?.Dispose(); } private class TestBroadcastClient : IDisposable From fe121211677c55e9c4f576b3582ace18e8285c70 Mon Sep 17 00:00:00 2001 From: Derrick Timmermans Date: Fri, 24 Sep 2021 22:10:23 +0200 Subject: [PATCH 23/37] Recreate pipe client when server dies --- .../IO/TestSceneGameplayEventBroadcaster.cs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Sentakki.Tests/IO/TestSceneGameplayEventBroadcaster.cs b/osu.Game.Rulesets.Sentakki.Tests/IO/TestSceneGameplayEventBroadcaster.cs index cdf6042b1..2a0e76ce6 100644 --- a/osu.Game.Rulesets.Sentakki.Tests/IO/TestSceneGameplayEventBroadcaster.cs +++ b/osu.Game.Rulesets.Sentakki.Tests/IO/TestSceneGameplayEventBroadcaster.cs @@ -91,7 +91,7 @@ protected override void Dispose(bool isDisposing) private class TestBroadcastClient : IDisposable { - private readonly NamedPipeClientStream pipeClient; + private NamedPipeClientStream pipeClient; private readonly SpriteText text; @@ -128,7 +128,14 @@ private void clientLoop() // Server has shut down if (packet == TransmissionData.Kill) - continue; + { + // On non-Windows platforms, the client doesn't automatically reconnect + // So we must recreate the client to ensure safety; + pipeClient.Dispose(); + pipeClient = new NamedPipeClientStream(".", "senPipe", + PipeDirection.In, PipeOptions.Asynchronous, + TokenImpersonationLevel.Impersonation); + } if (packet != TransmissionData.Empty) text.Text = packet.ToString(); From 37e0a8178ccb3a3876ad84d39b75f55f63e15065 Mon Sep 17 00:00:00 2001 From: Derrick Timmermans Date: Sat, 25 Sep 2021 14:08:17 +0200 Subject: [PATCH 24/37] Run test methods together in ctor This avoids test failures in headless tests --- .../IO/TestSceneGameplayEventBroadcaster.cs | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/osu.Game.Rulesets.Sentakki.Tests/IO/TestSceneGameplayEventBroadcaster.cs b/osu.Game.Rulesets.Sentakki.Tests/IO/TestSceneGameplayEventBroadcaster.cs index 2a0e76ce6..76de424da 100644 --- a/osu.Game.Rulesets.Sentakki.Tests/IO/TestSceneGameplayEventBroadcaster.cs +++ b/osu.Game.Rulesets.Sentakki.Tests/IO/TestSceneGameplayEventBroadcaster.cs @@ -21,17 +21,15 @@ public TestSceneGameplayEventBroadcaster() { Text = "Nothing here yet" }); - } - [SetUp] - public void SetUpClient() - { - // Dispose existing broadcaster and client - client?.Dispose(); - broadcaster?.Dispose(); + // Ideally these methods should be the tests themselves + // But they do not play well during headless tests, likely due to simultaneous execution + TestNormalOperation(); + TestOperationWithoutClient(); + TestOperationWithClientDisconnect(); + TestClientOperationWithServerReconnect(); } - [Test] public void TestNormalOperation() { AddStep("Start broadcaster", () => broadcaster = new GameplayEventBroadcaster()); @@ -47,7 +45,6 @@ public void TestNormalOperation() AddStep("Dispose broadcaster", () => broadcaster.Dispose()); } - [Test] public void TestOperationWithoutClient() { AddStep("Start broadcaster", () => broadcaster = new GameplayEventBroadcaster()); @@ -55,7 +52,6 @@ public void TestOperationWithoutClient() AddStep("Dispose broadcaster", () => broadcaster.Dispose()); } - [Test] public void TestOperationWithClientDisconnect() { AddStep("Start broadcaster", () => broadcaster = new GameplayEventBroadcaster()); @@ -68,7 +64,6 @@ public void TestOperationWithClientDisconnect() // This is just to ensure my sample client implementation holds up // So others can be confident they aren't getting a sample that doesn't work - [Test] public void TestClientOperationWithServerReconnect() { AddStep("Start broadcaster", () => broadcaster = new GameplayEventBroadcaster()); From 2c132ab7af273a89af63333fc47f65cdc4cb31b8 Mon Sep 17 00:00:00 2001 From: Derrick Timmermans Date: Sat, 25 Sep 2021 14:22:10 +0200 Subject: [PATCH 25/37] Fix non-windows exception not being handled --- .../IO/TestSceneGameplayEventBroadcaster.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Sentakki.Tests/IO/TestSceneGameplayEventBroadcaster.cs b/osu.Game.Rulesets.Sentakki.Tests/IO/TestSceneGameplayEventBroadcaster.cs index 76de424da..38ef698bf 100644 --- a/osu.Game.Rulesets.Sentakki.Tests/IO/TestSceneGameplayEventBroadcaster.cs +++ b/osu.Game.Rulesets.Sentakki.Tests/IO/TestSceneGameplayEventBroadcaster.cs @@ -135,7 +135,7 @@ private void clientLoop() if (packet != TransmissionData.Empty) text.Text = packet.ToString(); } - catch (OperationCanceledException) + catch { break; } From 4e82a605db5f837d75419c40b614afe5a4080460 Mon Sep 17 00:00:00 2001 From: Derrick Timmermans Date: Sat, 25 Sep 2021 14:25:10 +0200 Subject: [PATCH 26/37] Appease CodeFactor --- osu.Game.Rulesets.Sentakki/IO/GameplayEventBroadcaster.cs | 1 - osu.Game.Rulesets.Sentakki/IO/TransmissionProtocol.cs | 1 - osu.Game.Rulesets.Sentakki/UI/LanedPlayfield.cs | 1 - 3 files changed, 3 deletions(-) diff --git a/osu.Game.Rulesets.Sentakki/IO/GameplayEventBroadcaster.cs b/osu.Game.Rulesets.Sentakki/IO/GameplayEventBroadcaster.cs index 81d86e405..bc14df054 100644 --- a/osu.Game.Rulesets.Sentakki/IO/GameplayEventBroadcaster.cs +++ b/osu.Game.Rulesets.Sentakki/IO/GameplayEventBroadcaster.cs @@ -45,7 +45,6 @@ private void waitForConnectionCallBack(IAsyncResult result) if (queuedData != TransmissionData.Empty) Broadcast(queuedData); - } public void Broadcast(TransmissionData packet) diff --git a/osu.Game.Rulesets.Sentakki/IO/TransmissionProtocol.cs b/osu.Game.Rulesets.Sentakki/IO/TransmissionProtocol.cs index 5ad52cc6e..94bcd7daa 100644 --- a/osu.Game.Rulesets.Sentakki/IO/TransmissionProtocol.cs +++ b/osu.Game.Rulesets.Sentakki/IO/TransmissionProtocol.cs @@ -56,7 +56,6 @@ public TransmissionData(InfoType type, int value) public override string ToString() => Type.ToString() + " " + Value; - public override bool Equals(object obj) => obj is TransmissionData other && Equals(other); public bool Equals(TransmissionData other) => (Type == InfoType.Kill && other.Type == InfoType.Kill) || (Type == other.Type && Value == other.Value); diff --git a/osu.Game.Rulesets.Sentakki/UI/LanedPlayfield.cs b/osu.Game.Rulesets.Sentakki/UI/LanedPlayfield.cs index c4b2c1860..2b35d875a 100644 --- a/osu.Game.Rulesets.Sentakki/UI/LanedPlayfield.cs +++ b/osu.Game.Rulesets.Sentakki/UI/LanedPlayfield.cs @@ -19,7 +19,6 @@ namespace osu.Game.Rulesets.Sentakki.UI { public class LanedPlayfield : Playfield { - [Resolved] private GameplayEventBroadcaster eventBroadcaster { get; set; } From 4b2ba9a90c6db565ebc1d98c9c6f4c307cb90182 Mon Sep 17 00:00:00 2001 From: Derrick Timmermans Date: Sun, 26 Sep 2021 13:19:06 +0200 Subject: [PATCH 27/37] Make use of async operations --- .../IO/TestSceneGameplayEventBroadcaster.cs | 48 +++++++++------- .../IO/GameplayEventBroadcaster.cs | 57 +++++++++---------- 2 files changed, 53 insertions(+), 52 deletions(-) diff --git a/osu.Game.Rulesets.Sentakki.Tests/IO/TestSceneGameplayEventBroadcaster.cs b/osu.Game.Rulesets.Sentakki.Tests/IO/TestSceneGameplayEventBroadcaster.cs index 38ef698bf..25d09f717 100644 --- a/osu.Game.Rulesets.Sentakki.Tests/IO/TestSceneGameplayEventBroadcaster.cs +++ b/osu.Game.Rulesets.Sentakki.Tests/IO/TestSceneGameplayEventBroadcaster.cs @@ -2,6 +2,7 @@ using System.IO.Pipes; using System.Security.Principal; using System.Threading; +using System.Threading.Tasks; using NUnit.Framework; using osu.Framework.Graphics.Sprites; using osu.Game.Rulesets.Sentakki.IO; @@ -21,15 +22,9 @@ public TestSceneGameplayEventBroadcaster() { Text = "Nothing here yet" }); - - // Ideally these methods should be the tests themselves - // But they do not play well during headless tests, likely due to simultaneous execution - TestNormalOperation(); - TestOperationWithoutClient(); - TestOperationWithClientDisconnect(); - TestClientOperationWithServerReconnect(); } + [Test] public void TestNormalOperation() { AddStep("Start broadcaster", () => broadcaster = new GameplayEventBroadcaster()); @@ -45,6 +40,7 @@ public void TestNormalOperation() AddStep("Dispose broadcaster", () => broadcaster.Dispose()); } + [Test] public void TestOperationWithoutClient() { AddStep("Start broadcaster", () => broadcaster = new GameplayEventBroadcaster()); @@ -52,6 +48,7 @@ public void TestOperationWithoutClient() AddStep("Dispose broadcaster", () => broadcaster.Dispose()); } + [Test] public void TestOperationWithClientDisconnect() { AddStep("Start broadcaster", () => broadcaster = new GameplayEventBroadcaster()); @@ -64,6 +61,7 @@ public void TestOperationWithClientDisconnect() // This is just to ensure my sample client implementation holds up // So others can be confident they aren't getting a sample that doesn't work + [Test] public void TestClientOperationWithServerReconnect() { AddStep("Start broadcaster", () => broadcaster = new GameplayEventBroadcaster()); @@ -75,6 +73,7 @@ public void TestClientOperationWithServerReconnect() AddStep("Send message 1", () => broadcaster.Broadcast(new TransmissionData(TransmissionData.InfoType.HitPerfect, 3))); AddUntilStep("Client received message 1", () => text.Text == new TransmissionData(TransmissionData.InfoType.HitPerfect, 3).ToString()); AddStep("Dispose broadcaster", () => broadcaster.Dispose()); + AddStep("Client disconnect", () => client?.Dispose()); } protected override void Dispose(bool isDisposing) @@ -110,30 +109,36 @@ public TestBroadcastClient(SpriteText outputText) private readonly CancellationTokenSource cancellationTokenSource = new CancellationTokenSource(); private CancellationToken cancellationToken => cancellationTokenSource.Token; - private void clientLoop() + private async void clientLoop() { + byte[] buffer = new byte[1]; while (running) { try { if (!pipeClient.IsConnected) - pipeClient.ConnectAsync().Wait(cancellationToken); + await pipeClient.ConnectAsync(cancellationToken).ConfigureAwait(false); - TransmissionData packet = new TransmissionData((byte)pipeClient.ReadByte()); + int result = await pipeClient.ReadAsync(new Memory(buffer), cancellationToken).ConfigureAwait(false); - // Server has shut down - if (packet == TransmissionData.Kill) + if (result > 0) { - // On non-Windows platforms, the client doesn't automatically reconnect - // So we must recreate the client to ensure safety; - pipeClient.Dispose(); - pipeClient = new NamedPipeClientStream(".", "senPipe", - PipeDirection.In, PipeOptions.Asynchronous, - TokenImpersonationLevel.Impersonation); + TransmissionData packet = new TransmissionData(buffer[0]); + + // Server has shut down + if (packet == TransmissionData.Kill) + { + // On non-Windows platforms, the client doesn't automatically reconnect + // So we must recreate the client to ensure safety; + pipeClient.Dispose(); + pipeClient = new NamedPipeClientStream(".", "senPipe", + PipeDirection.In, PipeOptions.Asynchronous, + TokenImpersonationLevel.Impersonation); + } + + if (packet != TransmissionData.Empty) + text.Text = packet.ToString(); } - - if (packet != TransmissionData.Empty) - text.Text = packet.ToString(); } catch { @@ -146,6 +151,7 @@ public void Dispose() { running = false; cancellationTokenSource.Cancel(); + readThread.Join(); pipeClient.Dispose(); } } diff --git a/osu.Game.Rulesets.Sentakki/IO/GameplayEventBroadcaster.cs b/osu.Game.Rulesets.Sentakki/IO/GameplayEventBroadcaster.cs index bc14df054..50a76d89a 100644 --- a/osu.Game.Rulesets.Sentakki/IO/GameplayEventBroadcaster.cs +++ b/osu.Game.Rulesets.Sentakki/IO/GameplayEventBroadcaster.cs @@ -1,6 +1,8 @@ using System; using System.IO; using System.IO.Pipes; +using System.Threading; +using System.Threading.Tasks; namespace osu.Game.Rulesets.Sentakki.IO { @@ -16,70 +18,63 @@ public class GameplayEventBroadcaster : IDisposable public GameplayEventBroadcaster() { - pipeServer = new NamedPipeServerStream("senPipe", PipeDirection.InOut, 1, PipeTransmissionMode.Byte, PipeOptions.Asynchronous); - onClientDisconnected(); + pipeServer = new NamedPipeServerStream("senPipe", PipeDirection.Out, 1, PipeTransmissionMode.Byte, PipeOptions.Asynchronous); + attemptConnection(); } private bool isWaitingForClient; - private void onClientDisconnected() + private readonly CancellationTokenSource cancellationTokenSource = new CancellationTokenSource(); + private CancellationToken cancellationToken => cancellationTokenSource.Token; + + private async void attemptConnection() { if (isWaitingForClient) return; isWaitingForClient = true; - pipeServer.BeginWaitForConnection(new AsyncCallback(waitForConnectionCallBack), this); - } - private void waitForConnectionCallBack(IAsyncResult result) - { - try - { - pipeServer.EndWaitForConnection(result); - isWaitingForClient = false; - } - catch - { - // The server has been disposed, abort wait - return; - } + try { await pipeServer.WaitForConnectionAsync(cancellationToken).ConfigureAwait(false); } + catch (TaskCanceledException) { return; } + + isWaitingForClient = false; if (queuedData != TransmissionData.Empty) Broadcast(queuedData); } - public void Broadcast(TransmissionData packet) + private readonly byte[] buffer = new byte[1]; + + public async void Broadcast(TransmissionData packet) { + buffer[0] = packet.RawData; + if (isDisposed) throw new ObjectDisposedException(nameof(GameplayEventBroadcaster)); queuedData = packet; - if (!connectionValid()) return; - - pipeServer.WriteByte(packet.RawData); - queuedData = TransmissionData.Empty; - } - private bool connectionValid() - { - if (isWaitingForClient) return false; + if (isWaitingForClient) return; - try { pipeServer.WriteByte(0); } + try + { + await pipeServer.WriteAsync(new Memory(buffer), cancellationToken).ConfigureAwait(false); + } catch (IOException) { // The client has suddenly disconnected, we must disconnect on our end, and wait for a new connection. pipeServer.Disconnect(); - onClientDisconnected(); + attemptConnection(); - return false; + return; } - // We assume that connection is valid at this point - return true; + queuedData = TransmissionData.Empty; } public void Dispose() { isDisposed = true; + cancellationTokenSource.Cancel(); pipeServer.Dispose(); } } From 16b811516fbb3e96dc7d1060af9293e9d718ee00 Mon Sep 17 00:00:00 2001 From: Derrick Timmermans Date: Sun, 26 Sep 2021 13:43:53 +0200 Subject: [PATCH 28/37] Add resend test --- .../IO/TestSceneGameplayEventBroadcaster.cs | 43 ++++++++++++++----- 1 file changed, 33 insertions(+), 10 deletions(-) diff --git a/osu.Game.Rulesets.Sentakki.Tests/IO/TestSceneGameplayEventBroadcaster.cs b/osu.Game.Rulesets.Sentakki.Tests/IO/TestSceneGameplayEventBroadcaster.cs index 25d09f717..9fe32ee99 100644 --- a/osu.Game.Rulesets.Sentakki.Tests/IO/TestSceneGameplayEventBroadcaster.cs +++ b/osu.Game.Rulesets.Sentakki.Tests/IO/TestSceneGameplayEventBroadcaster.cs @@ -2,7 +2,6 @@ using System.IO.Pipes; using System.Security.Principal; using System.Threading; -using System.Threading.Tasks; using NUnit.Framework; using osu.Framework.Graphics.Sprites; using osu.Game.Rulesets.Sentakki.IO; @@ -27,8 +26,8 @@ public TestSceneGameplayEventBroadcaster() [Test] public void TestNormalOperation() { - AddStep("Start broadcaster", () => broadcaster = new GameplayEventBroadcaster()); - AddStep("Create Client", () => client = new TestBroadcastClient(text)); + AddStep("Start broadcaster", () => createBroadcaster()); + AddStep("Create Client", () => createTestClient()); AddUntilStep("Client connected", () => client.IsClientConnected); AddStep("Send message 1", () => broadcaster.Broadcast(new TransmissionData(TransmissionData.InfoType.HitPerfect, 3))); AddUntilStep("Client received message 1", () => text.Text == new TransmissionData(TransmissionData.InfoType.HitPerfect, 3).ToString()); @@ -36,14 +35,14 @@ public void TestNormalOperation() AddUntilStep("Client received message 2", () => text.Text == new TransmissionData(TransmissionData.InfoType.MetaEndPlay, 3).ToString()); AddStep("Send message 3", () => broadcaster.Broadcast(new TransmissionData(TransmissionData.InfoType.Miss, 3))); AddUntilStep("Client received message 3", () => text.Text == new TransmissionData(TransmissionData.InfoType.Miss, 3).ToString()); - AddStep("Client disconnect", () => client?.Dispose()); + AddStep("Dispose client", () => client?.Dispose()); AddStep("Dispose broadcaster", () => broadcaster.Dispose()); } [Test] public void TestOperationWithoutClient() { - AddStep("Start broadcaster", () => broadcaster = new GameplayEventBroadcaster()); + AddStep("Start broadcaster", () => createBroadcaster()); AddStep("Send message 1", () => broadcaster.Broadcast(new TransmissionData(TransmissionData.InfoType.HitPerfect, 3))); AddStep("Dispose broadcaster", () => broadcaster.Dispose()); } @@ -51,21 +50,33 @@ public void TestOperationWithoutClient() [Test] public void TestOperationWithClientDisconnect() { - AddStep("Start broadcaster", () => broadcaster = new GameplayEventBroadcaster()); - AddStep("Create Client", () => client = new TestBroadcastClient(text)); + AddStep("Start broadcaster", () => createBroadcaster()); + AddStep("Create Client", () => createTestClient()); AddUntilStep("Client connected", () => client.IsClientConnected); AddStep("Client disconnect", () => client?.Dispose()); AddStep("Send message 1", () => broadcaster.Broadcast(new TransmissionData(TransmissionData.InfoType.HitPerfect, 3))); AddStep("Dispose broadcaster", () => broadcaster.Dispose()); } + [Test] + public void TestRetryBroadcastOnClientReconnect() + { + AddStep("Start broadcaster", () => createBroadcaster()); + AddStep("Send message 1", () => broadcaster.Broadcast(new TransmissionData(TransmissionData.InfoType.HitPerfect, 3))); + AddStep("Create Client", () => createTestClient()); + AddUntilStep("Client connected", () => client.IsClientConnected); + AddUntilStep("Client received message 1", () => text.Text == new TransmissionData(TransmissionData.InfoType.HitPerfect, 3).ToString()); + AddStep("Dispose broadcaster", () => broadcaster.Dispose()); + AddStep("Dispose client", () => client?.Dispose()); + } + // This is just to ensure my sample client implementation holds up // So others can be confident they aren't getting a sample that doesn't work [Test] public void TestClientOperationWithServerReconnect() { - AddStep("Start broadcaster", () => broadcaster = new GameplayEventBroadcaster()); - AddStep("Create Client", () => client = new TestBroadcastClient(text)); + AddStep("Start broadcaster", () => createBroadcaster()); + AddStep("Create Client", () => createTestClient()); AddUntilStep("Client connected", () => client.IsClientConnected); AddStep("Dispose broadcaster", () => broadcaster.Dispose()); AddStep("Start new broadcaster", () => broadcaster = new GameplayEventBroadcaster()); @@ -73,7 +84,19 @@ public void TestClientOperationWithServerReconnect() AddStep("Send message 1", () => broadcaster.Broadcast(new TransmissionData(TransmissionData.InfoType.HitPerfect, 3))); AddUntilStep("Client received message 1", () => text.Text == new TransmissionData(TransmissionData.InfoType.HitPerfect, 3).ToString()); AddStep("Dispose broadcaster", () => broadcaster.Dispose()); - AddStep("Client disconnect", () => client?.Dispose()); + AddStep("Dispose client", () => client?.Dispose()); + } + + private void createBroadcaster() + { + broadcaster?.Dispose(); + broadcaster = new GameplayEventBroadcaster(); + } + + private void createTestClient() + { + client?.Dispose(); + client = new TestBroadcastClient(text); } protected override void Dispose(bool isDisposing) From 491f56d78d4ffba4bf689d93e2f4e87b9d9219e6 Mon Sep 17 00:00:00 2001 From: Derrick Timmermans Date: Sun, 26 Sep 2021 13:46:21 +0200 Subject: [PATCH 29/37] Account for OperationCanceledException --- .../IO/GameplayEventBroadcaster.cs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Sentakki/IO/GameplayEventBroadcaster.cs b/osu.Game.Rulesets.Sentakki/IO/GameplayEventBroadcaster.cs index 50a76d89a..695ae5d5f 100644 --- a/osu.Game.Rulesets.Sentakki/IO/GameplayEventBroadcaster.cs +++ b/osu.Game.Rulesets.Sentakki/IO/GameplayEventBroadcaster.cs @@ -1,6 +1,7 @@ using System; using System.IO; using System.IO.Pipes; +using System.Net.Sockets; using System.Threading; using System.Threading.Tasks; @@ -34,7 +35,13 @@ private async void attemptConnection() isWaitingForClient = true; try { await pipeServer.WaitForConnectionAsync(cancellationToken).ConfigureAwait(false); } - catch (TaskCanceledException) { return; } + catch (Exception e) + { + // The operation was canceled. Gracefully shutdown; + if (e is TaskCanceledException || (e is SocketException se && se.SocketErrorCode == SocketError.OperationAborted)) + return; + throw; + } isWaitingForClient = false; From 82c079d32a2d6250bfdc19a334cb6ed8896f3a0a Mon Sep 17 00:00:00 2001 From: Derrick Timmermans Date: Sun, 26 Sep 2021 14:03:35 +0200 Subject: [PATCH 30/37] New check for server disconnect A kill byte isn't received anymore --- .../IO/TestSceneGameplayEventBroadcaster.cs | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/osu.Game.Rulesets.Sentakki.Tests/IO/TestSceneGameplayEventBroadcaster.cs b/osu.Game.Rulesets.Sentakki.Tests/IO/TestSceneGameplayEventBroadcaster.cs index 9fe32ee99..a11cdd679 100644 --- a/osu.Game.Rulesets.Sentakki.Tests/IO/TestSceneGameplayEventBroadcaster.cs +++ b/osu.Game.Rulesets.Sentakki.Tests/IO/TestSceneGameplayEventBroadcaster.cs @@ -148,20 +148,20 @@ private async void clientLoop() { TransmissionData packet = new TransmissionData(buffer[0]); - // Server has shut down - if (packet == TransmissionData.Kill) - { - // On non-Windows platforms, the client doesn't automatically reconnect - // So we must recreate the client to ensure safety; - pipeClient.Dispose(); - pipeClient = new NamedPipeClientStream(".", "senPipe", - PipeDirection.In, PipeOptions.Asynchronous, - TokenImpersonationLevel.Impersonation); - } - if (packet != TransmissionData.Empty) text.Text = packet.ToString(); } + else if (result == 0) // End of stream reached, meaning that the server disconnected + { + text.Text = TransmissionData.Kill.ToString(); + + // On non-Windows platforms, the client doesn't automatically reconnect + // So we must recreate the client to ensure safety; + pipeClient.Dispose(); + pipeClient = new NamedPipeClientStream(".", "senPipe", + PipeDirection.In, PipeOptions.Asynchronous, + TokenImpersonationLevel.Impersonation); + } } catch { From 993849383d98977c305e941d9e07ff7204a40db6 Mon Sep 17 00:00:00 2001 From: Derrick Timmermans Date: Sun, 26 Sep 2021 14:21:54 +0200 Subject: [PATCH 31/37] Be specific in client exception handling --- .../IO/TestSceneGameplayEventBroadcaster.cs | 9 +++++++-- .../IO/GameplayEventBroadcaster.cs | 1 + 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Sentakki.Tests/IO/TestSceneGameplayEventBroadcaster.cs b/osu.Game.Rulesets.Sentakki.Tests/IO/TestSceneGameplayEventBroadcaster.cs index a11cdd679..687d13fe2 100644 --- a/osu.Game.Rulesets.Sentakki.Tests/IO/TestSceneGameplayEventBroadcaster.cs +++ b/osu.Game.Rulesets.Sentakki.Tests/IO/TestSceneGameplayEventBroadcaster.cs @@ -2,6 +2,7 @@ using System.IO.Pipes; using System.Security.Principal; using System.Threading; +using System.Threading.Tasks; using NUnit.Framework; using osu.Framework.Graphics.Sprites; using osu.Game.Rulesets.Sentakki.IO; @@ -163,9 +164,13 @@ private async void clientLoop() TokenImpersonationLevel.Impersonation); } } - catch + catch (Exception e) { - break; + // The operation was canceled. Gracefully shutdown; + if (e is TaskCanceledException || e is OperationCanceledException) + return; + + throw; } } } diff --git a/osu.Game.Rulesets.Sentakki/IO/GameplayEventBroadcaster.cs b/osu.Game.Rulesets.Sentakki/IO/GameplayEventBroadcaster.cs index 695ae5d5f..151d59b1b 100644 --- a/osu.Game.Rulesets.Sentakki/IO/GameplayEventBroadcaster.cs +++ b/osu.Game.Rulesets.Sentakki/IO/GameplayEventBroadcaster.cs @@ -40,6 +40,7 @@ private async void attemptConnection() // The operation was canceled. Gracefully shutdown; if (e is TaskCanceledException || (e is SocketException se && se.SocketErrorCode == SocketError.OperationAborted)) return; + throw; } From 8b52769649580c79127d92524c4698c3fd21e051 Mon Sep 17 00:00:00 2001 From: Derrick Timmermans Date: Sun, 26 Sep 2021 14:22:33 +0200 Subject: [PATCH 32/37] Remove unused isDisposed flag --- osu.Game.Rulesets.Sentakki/IO/GameplayEventBroadcaster.cs | 6 ------ 1 file changed, 6 deletions(-) diff --git a/osu.Game.Rulesets.Sentakki/IO/GameplayEventBroadcaster.cs b/osu.Game.Rulesets.Sentakki/IO/GameplayEventBroadcaster.cs index 151d59b1b..6d17e021a 100644 --- a/osu.Game.Rulesets.Sentakki/IO/GameplayEventBroadcaster.cs +++ b/osu.Game.Rulesets.Sentakki/IO/GameplayEventBroadcaster.cs @@ -15,8 +15,6 @@ public class GameplayEventBroadcaster : IDisposable // In the event that a broadcast fails, we can resend this message once a new connection is established. private TransmissionData queuedData; - private bool isDisposed; - public GameplayEventBroadcaster() { pipeServer = new NamedPipeServerStream("senPipe", PipeDirection.Out, 1, PipeTransmissionMode.Byte, PipeOptions.Asynchronous); @@ -56,9 +54,6 @@ public async void Broadcast(TransmissionData packet) { buffer[0] = packet.RawData; - if (isDisposed) - throw new ObjectDisposedException(nameof(GameplayEventBroadcaster)); - queuedData = packet; if (isWaitingForClient) return; @@ -81,7 +76,6 @@ public async void Broadcast(TransmissionData packet) public void Dispose() { - isDisposed = true; cancellationTokenSource.Cancel(); pipeServer.Dispose(); } From 0901b8c8638f35a286ce41db0ad7a92c59b14f7a Mon Sep 17 00:00:00 2001 From: Derrick Timmermans Date: Sun, 26 Sep 2021 14:55:36 +0200 Subject: [PATCH 33/37] Run tests on all platforms --- .github/workflows/dotnetcore.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/dotnetcore.yml b/.github/workflows/dotnetcore.yml index f7fee060d..807ce3a12 100644 --- a/.github/workflows/dotnetcore.yml +++ b/.github/workflows/dotnetcore.yml @@ -9,7 +9,10 @@ on: jobs: build: name: Build and Test - runs-on: ubuntu-latest + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ ubuntu-latest, macos-latest, windows-latest ] steps: - name: Checkout repository From fc87a3f9babdd2cbd2f65b009a7f4bdc7229f410 Mon Sep 17 00:00:00 2001 From: Derrick Timmermans Date: Fri, 4 Feb 2022 16:06:03 +0100 Subject: [PATCH 34/37] Use broadcast method provided by DrawableSentakkiRuleset --- .../UI/DrawableSentakkiRuleset.cs | 26 +++++++++++-------- osu.Game.Rulesets.Sentakki/UI/Lane.cs | 17 +++++------- .../UI/LanedPlayfield.cs | 4 +-- .../UI/SentakkiPlayfield.cs | 4 --- 4 files changed, 24 insertions(+), 27 deletions(-) diff --git a/osu.Game.Rulesets.Sentakki/UI/DrawableSentakkiRuleset.cs b/osu.Game.Rulesets.Sentakki/UI/DrawableSentakkiRuleset.cs index 8973a0371..c725978db 100644 --- a/osu.Game.Rulesets.Sentakki/UI/DrawableSentakkiRuleset.cs +++ b/osu.Game.Rulesets.Sentakki/UI/DrawableSentakkiRuleset.cs @@ -31,17 +31,7 @@ public DrawableSentakkiRuleset(SentakkiRuleset ruleset, IBeatmap beatmap, IReadO protected override void LoadComplete() { (Config as SentakkiRulesetConfigManager)?.BindWith(SentakkiRulesetSettings.LaneInputMode, laneInputMode); - } - - private GameplayEventBroadcaster eventBroadcaster; - - protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) - { - var dependencies = new DependencyContainer(base.CreateChildDependencies(parent)); - - dependencies.CacheAs(eventBroadcaster = new GameplayEventBroadcaster()); - - return dependencies; + TryBroadcastGameplayEvent(new TransmissionData(TransmissionData.InfoType.MetaStartPlay, 0)); } // Input specifics (sensor/button) for replay and gameplay @@ -58,6 +48,20 @@ protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnl public double GameplaySpeed => speedAdjustmentTrack.Rate; + /// Network broadcasting stuff + private readonly GameplayEventBroadcaster eventBroadcaster = new GameplayEventBroadcaster(); + + + /// + /// Tries broadcasting an gameplay event. + /// This depends on user settings, so may not guarantee successful transmission + /// + /// The packet to be transmitted + public void TryBroadcastGameplayEvent(TransmissionData packet) + { + eventBroadcaster.Broadcast(packet); + } + // Default stuff protected override Playfield CreatePlayfield() => new SentakkiPlayfield(); diff --git a/osu.Game.Rulesets.Sentakki/UI/Lane.cs b/osu.Game.Rulesets.Sentakki/UI/Lane.cs index 87277c7be..784855be8 100644 --- a/osu.Game.Rulesets.Sentakki/UI/Lane.cs +++ b/osu.Game.Rulesets.Sentakki/UI/Lane.cs @@ -19,9 +19,6 @@ namespace osu.Game.Rulesets.Sentakki.UI { public class Lane : Playfield, IKeyBindingHandler { - [Resolved] - private GameplayEventBroadcaster eventBroadcaster { get; set; } - public int LaneNumber { get; set; } public Action OnLoaded; @@ -42,15 +39,15 @@ protected override void Update() updateInputState(); } - private DrawableSentakkiRuleset drawableSentakkiRuleset; - private SentakkiRulesetConfigManager sentakkiRulesetConfig; + [Resolved] + private DrawableSentakkiRuleset drawableSentakkiRuleset { get; set; } + + [Resolved] + private SentakkiRulesetConfigManager sentakkiRulesetConfig { get; set; } [BackgroundDependencyLoader(true)] - private void load(DrawableSentakkiRuleset drawableRuleset, SentakkiRulesetConfigManager sentakkiRulesetConfigManager) + private void load() { - drawableSentakkiRuleset = drawableRuleset; - sentakkiRulesetConfig = sentakkiRulesetConfigManager; - RegisterPool(8); RegisterPool(8); @@ -122,7 +119,7 @@ private void handleKeyPress(ValueChangedEvent keys) if (keys.NewValue > keys.OldValue) { - eventBroadcaster.Broadcast(new TransmissionData(TransmissionData.InfoType.LanePressed, LaneNumber)); + drawableSentakkiRuleset.TryBroadcastGameplayEvent(new TransmissionData(TransmissionData.InfoType.LanePressed, LaneNumber)); SentakkiActionInputManager.TriggerPressed(SentakkiAction.Key1 + LaneNumber); } } diff --git a/osu.Game.Rulesets.Sentakki/UI/LanedPlayfield.cs b/osu.Game.Rulesets.Sentakki/UI/LanedPlayfield.cs index 2b35d875a..456005b2d 100644 --- a/osu.Game.Rulesets.Sentakki/UI/LanedPlayfield.cs +++ b/osu.Game.Rulesets.Sentakki/UI/LanedPlayfield.cs @@ -20,7 +20,7 @@ namespace osu.Game.Rulesets.Sentakki.UI public class LanedPlayfield : Playfield { [Resolved] - private GameplayEventBroadcaster eventBroadcaster { get; set; } + private DrawableSentakkiRuleset sentakkiRuleset { get; set; } public readonly List Lanes = new List(); @@ -139,7 +139,7 @@ private void onNewResult(DrawableHitObject judgedObject, JudgementResult result) break; } - eventBroadcaster.Broadcast(new TransmissionData(resultType, laned.HitObject.Lane)); + sentakkiRuleset.TryBroadcastGameplayEvent(new TransmissionData(resultType, laned.HitObject.Lane)); var explosion = explosionPool.Get(e => e.Apply(laned.HitObject)); explosionLayer.Add(explosion); diff --git a/osu.Game.Rulesets.Sentakki/UI/SentakkiPlayfield.cs b/osu.Game.Rulesets.Sentakki/UI/SentakkiPlayfield.cs index d4e6ce099..ff02ff659 100644 --- a/osu.Game.Rulesets.Sentakki/UI/SentakkiPlayfield.cs +++ b/osu.Game.Rulesets.Sentakki/UI/SentakkiPlayfield.cs @@ -23,9 +23,6 @@ namespace osu.Game.Rulesets.Sentakki.UI [Cached] public class SentakkiPlayfield : Playfield { - [Resolved] - private GameplayEventBroadcaster gameplayEventBroadcaster { get; set; } - private readonly Container judgementLayer; private readonly DrawablePool judgementPool; @@ -116,7 +113,6 @@ protected override void LoadComplete() skin.BindValueChanged(_ => changePlayfieldAccent(), true); ringColor.BindValueChanged(_ => changePlayfieldAccent(), true); - gameplayEventBroadcaster.Broadcast(new TransmissionData(TransmissionData.InfoType.MetaStartPlay, 0)); } protected override HitObjectLifetimeEntry CreateLifetimeEntry(HitObject hitObject) => new SentakkiHitObjectLifetimeEntry(hitObject, sentakkiRulesetConfig, drawableSentakkiRuleset); From d07418b23094b0b40aece2a6a6e6c30f6059aef4 Mon Sep 17 00:00:00 2001 From: Derrick Timmermans Date: Fri, 4 Feb 2022 16:32:58 +0100 Subject: [PATCH 35/37] Add ruleset option to turn on/off IPC --- .../IO/TestSceneGameplayEventBroadcaster.cs | 2 +- osu.Game.Rulesets.Sentakki.Tests/TestSceneOsuGame.cs | 5 +++++ .../Configurations/SentakkiRulesetConfigManager.cs | 4 +++- osu.Game.Rulesets.Sentakki/IO/GameplayEventBroadcaster.cs | 1 + osu.Game.Rulesets.Sentakki/UI/DrawableSentakkiRuleset.cs | 6 +++++- osu.Game.Rulesets.Sentakki/UI/SentakkiSettingsSubsection.cs | 5 +++++ 6 files changed, 20 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Sentakki.Tests/IO/TestSceneGameplayEventBroadcaster.cs b/osu.Game.Rulesets.Sentakki.Tests/IO/TestSceneGameplayEventBroadcaster.cs index 687d13fe2..d5196fc29 100644 --- a/osu.Game.Rulesets.Sentakki.Tests/IO/TestSceneGameplayEventBroadcaster.cs +++ b/osu.Game.Rulesets.Sentakki.Tests/IO/TestSceneGameplayEventBroadcaster.cs @@ -107,7 +107,7 @@ protected override void Dispose(bool isDisposing) broadcaster?.Dispose(); } - private class TestBroadcastClient : IDisposable + public class TestBroadcastClient : IDisposable { private NamedPipeClientStream pipeClient; diff --git a/osu.Game.Rulesets.Sentakki.Tests/TestSceneOsuGame.cs b/osu.Game.Rulesets.Sentakki.Tests/TestSceneOsuGame.cs index 918c558c6..45ecbfd35 100644 --- a/osu.Game.Rulesets.Sentakki.Tests/TestSceneOsuGame.cs +++ b/osu.Game.Rulesets.Sentakki.Tests/TestSceneOsuGame.cs @@ -2,8 +2,10 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Sprites; using osu.Game.Online.API; using osu.Game.Online.API.Requests.Responses; +using osu.Game.Rulesets.Sentakki.Tests.IO; using osu.Game.Tests.Visual; using osu.Game.Users; using osuTK.Graphics; @@ -12,9 +14,12 @@ namespace osu.Game.Rulesets.Sentakki.Tests { public class TestSceneOsuGame : OsuTestScene { + private SpriteText text = new SpriteText(); + private TestSceneGameplayEventBroadcaster.TestBroadcastClient testClient; [BackgroundDependencyLoader] private void load() { + testClient = new TestSceneGameplayEventBroadcaster.TestBroadcastClient(text); Children = new Drawable[] { new Box diff --git a/osu.Game.Rulesets.Sentakki/Configurations/SentakkiRulesetConfigManager.cs b/osu.Game.Rulesets.Sentakki/Configurations/SentakkiRulesetConfigManager.cs index 0040bbbf7..18c9d4915 100644 --- a/osu.Game.Rulesets.Sentakki/Configurations/SentakkiRulesetConfigManager.cs +++ b/osu.Game.Rulesets.Sentakki/Configurations/SentakkiRulesetConfigManager.cs @@ -24,6 +24,7 @@ protected override void InitialiseDefaults() SetDefault(SentakkiRulesetSettings.LaneInputMode, LaneInputMode.Button); SetDefault(SentakkiRulesetSettings.SnakingSlideBody, true); SetDefault(SentakkiRulesetSettings.DetailedJudgements, false); + SetDefault(SentakkiRulesetSettings.GameplayIPC, false); } } @@ -51,6 +52,7 @@ public enum SentakkiRulesetSettings TouchAnimationDuration, LaneInputMode, SnakingSlideBody, - DetailedJudgements + DetailedJudgements, + GameplayIPC, } } diff --git a/osu.Game.Rulesets.Sentakki/IO/GameplayEventBroadcaster.cs b/osu.Game.Rulesets.Sentakki/IO/GameplayEventBroadcaster.cs index 6d17e021a..9a1216f17 100644 --- a/osu.Game.Rulesets.Sentakki/IO/GameplayEventBroadcaster.cs +++ b/osu.Game.Rulesets.Sentakki/IO/GameplayEventBroadcaster.cs @@ -61,6 +61,7 @@ public async void Broadcast(TransmissionData packet) try { await pipeServer.WriteAsync(new Memory(buffer), cancellationToken).ConfigureAwait(false); + Console.WriteLine(packet.ToString()); } catch (IOException) { diff --git a/osu.Game.Rulesets.Sentakki/UI/DrawableSentakkiRuleset.cs b/osu.Game.Rulesets.Sentakki/UI/DrawableSentakkiRuleset.cs index c725978db..879758ed7 100644 --- a/osu.Game.Rulesets.Sentakki/UI/DrawableSentakkiRuleset.cs +++ b/osu.Game.Rulesets.Sentakki/UI/DrawableSentakkiRuleset.cs @@ -31,6 +31,7 @@ public DrawableSentakkiRuleset(SentakkiRuleset ruleset, IBeatmap beatmap, IReadO protected override void LoadComplete() { (Config as SentakkiRulesetConfigManager)?.BindWith(SentakkiRulesetSettings.LaneInputMode, laneInputMode); + (Config as SentakkiRulesetConfigManager)?.BindWith(SentakkiRulesetSettings.GameplayIPC, ipcEnabled); TryBroadcastGameplayEvent(new TransmissionData(TransmissionData.InfoType.MetaStartPlay, 0)); } @@ -51,6 +52,8 @@ protected override void LoadComplete() /// Network broadcasting stuff private readonly GameplayEventBroadcaster eventBroadcaster = new GameplayEventBroadcaster(); + private readonly Bindable ipcEnabled = new Bindable(); + /// /// Tries broadcasting an gameplay event. @@ -59,7 +62,8 @@ protected override void LoadComplete() /// The packet to be transmitted public void TryBroadcastGameplayEvent(TransmissionData packet) { - eventBroadcaster.Broadcast(packet); + if (ipcEnabled.Value) + eventBroadcaster.Broadcast(packet); } // Default stuff diff --git a/osu.Game.Rulesets.Sentakki/UI/SentakkiSettingsSubsection.cs b/osu.Game.Rulesets.Sentakki/UI/SentakkiSettingsSubsection.cs index 74a946870..27c9dbc90 100644 --- a/osu.Game.Rulesets.Sentakki/UI/SentakkiSettingsSubsection.cs +++ b/osu.Game.Rulesets.Sentakki/UI/SentakkiSettingsSubsection.cs @@ -76,6 +76,11 @@ private void load() LabelText = "Lane input mode (Doesn't apply to touch)", Current = config.GetBindable(SentakkiRulesetSettings.LaneInputMode) }, + new SettingsCheckbox{ + LabelText = "Broadcast gameplay events", + TooltipText = "Allows third party programs to receive events. (RGB lights or whatnot)", + Current = config.GetBindable(SentakkiRulesetSettings.GameplayIPC) + }, }; } From ba0075f6d758f1af54796b7945c652283c3b4935 Mon Sep 17 00:00:00 2001 From: Derrick Timmermans Date: Fri, 4 Feb 2022 16:33:43 +0100 Subject: [PATCH 36/37] Remove debug code --- osu.Game.Rulesets.Sentakki.Tests/TestSceneOsuGame.cs | 3 --- osu.Game.Rulesets.Sentakki/IO/GameplayEventBroadcaster.cs | 1 - 2 files changed, 4 deletions(-) diff --git a/osu.Game.Rulesets.Sentakki.Tests/TestSceneOsuGame.cs b/osu.Game.Rulesets.Sentakki.Tests/TestSceneOsuGame.cs index 45ecbfd35..b1e6bb831 100644 --- a/osu.Game.Rulesets.Sentakki.Tests/TestSceneOsuGame.cs +++ b/osu.Game.Rulesets.Sentakki.Tests/TestSceneOsuGame.cs @@ -14,12 +14,9 @@ namespace osu.Game.Rulesets.Sentakki.Tests { public class TestSceneOsuGame : OsuTestScene { - private SpriteText text = new SpriteText(); - private TestSceneGameplayEventBroadcaster.TestBroadcastClient testClient; [BackgroundDependencyLoader] private void load() { - testClient = new TestSceneGameplayEventBroadcaster.TestBroadcastClient(text); Children = new Drawable[] { new Box diff --git a/osu.Game.Rulesets.Sentakki/IO/GameplayEventBroadcaster.cs b/osu.Game.Rulesets.Sentakki/IO/GameplayEventBroadcaster.cs index 9a1216f17..6d17e021a 100644 --- a/osu.Game.Rulesets.Sentakki/IO/GameplayEventBroadcaster.cs +++ b/osu.Game.Rulesets.Sentakki/IO/GameplayEventBroadcaster.cs @@ -61,7 +61,6 @@ public async void Broadcast(TransmissionData packet) try { await pipeServer.WriteAsync(new Memory(buffer), cancellationToken).ConfigureAwait(false); - Console.WriteLine(packet.ToString()); } catch (IOException) { From 93d827b600c15f6a35e8b1fe3ad8bc5eb5c9b2c4 Mon Sep 17 00:00:00 2001 From: Derrick Timmermans Date: Wed, 9 Feb 2022 00:15:04 +0100 Subject: [PATCH 37/37] Send startplay event asap --- osu.Game.Rulesets.Sentakki/UI/DrawableSentakkiRuleset.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Sentakki/UI/DrawableSentakkiRuleset.cs b/osu.Game.Rulesets.Sentakki/UI/DrawableSentakkiRuleset.cs index 879758ed7..cf89f0739 100644 --- a/osu.Game.Rulesets.Sentakki/UI/DrawableSentakkiRuleset.cs +++ b/osu.Game.Rulesets.Sentakki/UI/DrawableSentakkiRuleset.cs @@ -28,7 +28,8 @@ public DrawableSentakkiRuleset(SentakkiRuleset ruleset, IBeatmap beatmap, IReadO mod.ApplyToTrack(speedAdjustmentTrack); } - protected override void LoadComplete() + [BackgroundDependencyLoader] + private void load() { (Config as SentakkiRulesetConfigManager)?.BindWith(SentakkiRulesetSettings.LaneInputMode, laneInputMode); (Config as SentakkiRulesetConfigManager)?.BindWith(SentakkiRulesetSettings.GameplayIPC, ipcEnabled); @@ -54,7 +55,6 @@ protected override void LoadComplete() private readonly Bindable ipcEnabled = new Bindable(); - /// /// Tries broadcasting an gameplay event. /// This depends on user settings, so may not guarantee successful transmission