feat: implement multiplayer session state management and add invalid port exception

This commit is contained in:
2026-08-25 02:10:54 +02:00
parent 988f1102f8
commit 1445fc014f
7 changed files with 64 additions and 16 deletions
@@ -1,6 +1,7 @@
namespace Game.Features.Multiplayer.Contracts.Exceptions; namespace Game.Features.Multiplayer.Contracts.Exceptions;
/// <summary> /// <summary>
/// Thrown when trying to host or join while already connected to a session. /// Thrown when trying to host or join while connecting or already connected to a session.
/// </summary> /// </summary>
public sealed class MultiplayerAlreadyConnectedException() : MultiplayerException("Already connected to a session."); public sealed class MultiplayerAlreadyConnectedException()
: MultiplayerException("Already connected to or connecting with a session.");
@@ -0,0 +1,4 @@
namespace Game.Features.Multiplayer.Contracts.Exceptions;
public sealed class MultiplayerInvalidPortException(int port)
: MultiplayerException($"Invalid port {port}. Port must be between 1 and 65535.");
@@ -0,0 +1 @@
uid://d3n8p5w2y7c4h
@@ -8,8 +8,7 @@ public sealed class MultiplayerSession
public const int MaxPlayers = 4; public const int MaxPlayers = 4;
public IReadOnlyCollection<PeerId> PeerIds => _peerIds; public IReadOnlyCollection<PeerId> PeerIds => _peerIds;
public bool IsFull => _peerIds.Count >= MaxPlayers; public bool IsFull => _peerIds.Count >= MaxPlayers;
public bool IsHosting { get; set; } public MultiplayerSessionState State { get; private set; } = MultiplayerSessionState.Disconnected;
public bool IsConnected { get; set; }
private readonly HashSet<PeerId> _peerIds = []; private readonly HashSet<PeerId> _peerIds = [];
@@ -17,10 +16,25 @@ public sealed class MultiplayerSession
public void RemovePeer(PeerId peerId) => _peerIds.Remove(peerId); public void RemovePeer(PeerId peerId) => _peerIds.Remove(peerId);
public void StartHosting(PeerId hostPeerId)
{
_peerIds.Add(hostPeerId);
State = MultiplayerSessionState.Hosting;
}
public void StartConnecting()
{
State = MultiplayerSessionState.Connecting;
}
public void MarkConnected()
{
State = MultiplayerSessionState.Connected;
}
public void Reset() public void Reset()
{ {
_peerIds.Clear(); _peerIds.Clear();
IsHosting = false; State = MultiplayerSessionState.Disconnected;
IsConnected = false;
} }
} }
@@ -0,0 +1,9 @@
namespace Game.Features.Multiplayer.Domain;
public enum MultiplayerSessionState
{
Disconnected,
Hosting,
Connecting,
Connected
}
@@ -0,0 +1 @@
uid://b7m2q9r4v6x1k
@@ -8,6 +8,9 @@ namespace Game.Features.Multiplayer.Godot;
public partial class MultiplayerController : Node, IMultiplayerPeer public partial class MultiplayerController : Node, IMultiplayerPeer
{ {
private const int MinPort = 1;
private const int MaxPort = 65535;
private readonly MultiplayerSession _session = new(); private readonly MultiplayerSession _session = new();
private IMultiplayerPeerFactory _multiplayerPeerFactory; private IMultiplayerPeerFactory _multiplayerPeerFactory;
@@ -33,29 +36,45 @@ public partial class MultiplayerController : Node, IMultiplayerPeer
private void OnConnectedToServer() private void OnConnectedToServer()
{ {
_session.MarkConnected();
Connected?.Invoke(); Connected?.Invoke();
} }
private void OnConnectionFailed() private void OnConnectionFailed()
{ {
_session.Reset(); ResetConnection();
ConnectionFailed?.Invoke(); ConnectionFailed?.Invoke();
} }
private void OnServerDisconnected() private void OnServerDisconnected()
{ {
_session.Reset(); ResetConnection();
Disconnected?.Invoke(); Disconnected?.Invoke();
} }
private static void ValidatePort(int port)
{
if (port is < MinPort or > MaxPort)
{
throw new MultiplayerInvalidPortException(port);
}
}
private void ResetConnection()
{
Multiplayer.MultiplayerPeer?.Close();
Multiplayer.MultiplayerPeer = null;
_session.Reset();
}
private void AssertNoActiveSession() private void AssertNoActiveSession()
{ {
if (_session.IsHosting) if (_session.State == MultiplayerSessionState.Hosting)
{ {
throw new MultiplayerAlreadyHostingException(); throw new MultiplayerAlreadyHostingException();
} }
if (_session.IsConnected) if (_session.State is MultiplayerSessionState.Connecting or MultiplayerSessionState.Connected)
{ {
throw new MultiplayerAlreadyConnectedException(); throw new MultiplayerAlreadyConnectedException();
} }
@@ -68,26 +87,25 @@ public partial class MultiplayerController : Node, IMultiplayerPeer
public void Host(int port) public void Host(int port)
{ {
ValidatePort(port);
AssertNoActiveSession(); AssertNoActiveSession();
Multiplayer.MultiplayerPeer = _multiplayerPeerFactory.CreateHost(port, MultiplayerSession.MaxPlayers - 1); Multiplayer.MultiplayerPeer = _multiplayerPeerFactory.CreateHost(port, MultiplayerSession.MaxPlayers - 1);
_session.AddPeer(new PeerId(Multiplayer.GetUniqueId())); _session.StartHosting(new PeerId(Multiplayer.GetUniqueId()));
_session.IsHosting = true;
} }
public void Join(string address, int port) public void Join(string address, int port)
{ {
ValidatePort(port);
AssertNoActiveSession(); AssertNoActiveSession();
Multiplayer.MultiplayerPeer = _multiplayerPeerFactory.CreateClient(address, port); Multiplayer.MultiplayerPeer = _multiplayerPeerFactory.CreateClient(address, port);
_session.IsConnected = true; _session.StartConnecting();
} }
public void Disconnect() public void Disconnect()
{ {
Multiplayer.MultiplayerPeer?.Close(); ResetConnection();
Multiplayer.MultiplayerPeer = null;
_session.Reset();
} }
public override void _Ready() public override void _Ready()