Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
0db049c
feat(online): validate handshake and trace position flow
aram-devdocs Jul 22, 2026
56319a7
fix(net): require capabilities in accepted server hello
aram-devdocs Jul 22, 2026
77a7e88
fix(online): harden handshake retries
aram-devdocs Jul 22, 2026
2391919
fix(net): ignore stale handshake events
aram-devdocs Jul 22, 2026
4377290
fix(server): throttle duplicate client hello
aram-devdocs Jul 23, 2026
f092a1d
fix(net): preserve client reconnect liveness
aram-devdocs Jul 23, 2026
c955030
fix(server): bound hello admission work
aram-devdocs Jul 23, 2026
984b46f
fix(server): separate hello clock domains
aram-devdocs Jul 23, 2026
3f39e53
fix(net): harden connection restart lifecycle
aram-devdocs Jul 23, 2026
9a5c0b7
fix(online): bound client hello lifecycle
aram-devdocs Jul 23, 2026
fc97a8b
fix(online): harden transport admission limits
aram-devdocs Jul 23, 2026
5d99a58
test(online): cover production transport paths
aram-devdocs Jul 23, 2026
f701fdf
fix(server): bound new handshake admissions
aram-devdocs Jul 23, 2026
a44df12
test(persist): cover concurrent admission capacity
aram-devdocs Jul 23, 2026
8c88901
fix(server): respect admission window in load fixture
aram-devdocs Jul 23, 2026
91ab80b
Merge remote-tracking branch 'origin/dev' into feat/15-ingame-handshake
aram-devdocs Jul 23, 2026
7dcbf41
fix(server): prevent superseded session reclaim
aram-devdocs Jul 23, 2026
9d2b5db
fix(server): bound rotating session admission
aram-devdocs Jul 23, 2026
ed74c92
fix(server): harden reconnect admission state
aram-devdocs Jul 23, 2026
5b31d9d
fix(smoke): retry transient hello admission
aram-devdocs Jul 23, 2026
bca29f5
test(smoke): prove transient hello retry
aram-devdocs Jul 23, 2026
72cb52c
fix(server): retain reconnect admission cooldown
aram-devdocs Jul 23, 2026
88d215f
fix(server): bound transport poll admission
aram-devdocs Jul 23, 2026
e13b4ce
fix(server): bound transport maintenance fairly
aram-devdocs Jul 23, 2026
b8f8fe1
fix(net): quarantine retired peer slots
aram-devdocs Jul 24, 2026
20ee901
fix(net): enforce peer connection numbers
aram-devdocs Jul 24, 2026
9d0c9ff
docs(agents): record connection number semantics
aram-devdocs Jul 24, 2026
3bf0e4a
fix(net): ignore stale reconnect requests
aram-devdocs Jul 24, 2026
b21d535
fix(net): retain reconnect ordering through slot retry
aram-devdocs Jul 24, 2026
178650f
fix(server): bound snapshot and AoI work
aram-devdocs Jul 24, 2026
dd14b7d
fix(server): bound mooring and recipient work
aram-devdocs Jul 24, 2026
4fcca54
fix(server): bound dense population work
aram-devdocs Jul 24, 2026
4518e54
test(server): prove bounded scheduler recovery
aram-devdocs Jul 24, 2026
5c4514d
fix(server): harden fanout and persistence retries
aram-devdocs Jul 24, 2026
850aced
fix(server): validate cadence and share fanout audiences
aram-devdocs Jul 24, 2026
b6699e1
fix(server): preserve fanout identities across wrap
aram-devdocs Jul 24, 2026
d16ff43
test(server): exhaust snapshot recurrence proof
aram-devdocs Jul 24, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .agents/lessons-learned.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,4 @@ fact.
2026-07-21: A Harmony POSTFIX on the game load method cannot stall the loading screen: it runs only after the load method returns, so a stuck load means the hook never fired (a fired-but-slow postfix would show as a hang after the screen clears, not a frozen loading screen). Still, wrap the event invoke in try/catch so a subscriber exception can never propagate back into the game's load and abort it.
2026-07-22: Bash fixture runners must remove CR from .env input before sourcing because Windows checkout conversion otherwise leaves carriage returns in exported values under POSIX shells.
2026-07-22: SaveLoadManager.readyToSave is the common static world-ready marker set by both StartMenu new-game and continue coroutines; LoadGame runs only for continue, so API readiness must poll readyToSave rather than depend on the LoadGame postfix.
2026-07-24: LiteNetLib 1.3.1 connected packets use a two-bit ConnectionNumber: stale mismatches are rejected before liveness or state handling, while user data, Ping, Pong, and Disconnect carry the active number.
16 changes: 14 additions & 2 deletions packages/net/Sailwind.Online.Net/ITransport.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,26 @@ public interface ITransport
/// <summary>True while the current peer is fully connected, i.e. safe to send on.</summary>
bool IsPeerConnected { get; }

/// <summary>
/// Maximum payload bytes the current peer can send as one unreliable packet, or zero when
/// there is no current peer.
/// </summary>
int MaxUnreliablePayloadSize { get; }

/// <summary>Round-trip estimate in milliseconds for the current peer, or -1 when there is none.</summary>
int Ping { get; }

/// <summary>Bring the manager up. Returns false when the socket cannot bind.</summary>
bool Start();

/// <summary>Open (or re-open) the single peer to <paramref name="host"/>:<paramref name="port"/> with the connect key.</summary>
void Connect(string host, int port, string key);
/// <summary>
/// Open (or keep opening) the single peer to <paramref name="host"/>:<paramref name="port"/>
/// with the connect key. Returns true when a current peer exists after the attempt.
/// </summary>
bool Connect(string host, int port, string key);

/// <summary>Immediately drop the current peer without reporting a network-originated disconnect.</summary>
void DropPeer();

/// <summary>Send one datagram to the current peer with the given delivery method.</summary>
void Send(byte[] data, DeliveryMethod deliveryMethod);
Expand Down
106 changes: 97 additions & 9 deletions packages/net/Sailwind.Online.Net/LiteNetLibTransport.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ public sealed class LiteNetLibTransport : ITransport
{
private readonly EventBasedNetListener _listener = new EventBasedNetListener();
private readonly NetManager _manager;
private NetPeer? _peer;
private readonly CurrentPeerSlot<NetPeer> _peer = new CurrentPeerSlot<NetPeer>();

public LiteNetLibTransport()
{
Expand All @@ -40,12 +40,31 @@ public bool IsRunning

public bool IsPeerConnected
{
get { return _peer != null && _peer.ConnectionState == ConnectionState.Connected; }
get
{
NetPeer? peer = _peer.Value;
return peer != null && peer.ConnectionState == ConnectionState.Connected;
}
}

public int MaxUnreliablePayloadSize
{
get
{
NetPeer? peer = _peer.Value;
return peer != null
? peer.GetMaxSinglePacketSize(DeliveryMethod.Unreliable)
: 0;
}
}

public int Ping
{
get { return _peer != null ? _peer.Ping : -1; }
get
{
NetPeer? peer = _peer.Value;
return peer != null ? peer.Ping : -1;
}
}

public event Action? PeerConnected;
Expand All @@ -58,14 +77,25 @@ public bool Start()
return _manager.Start();
}

public void Connect(string host, int port, string key)
public bool Connect(string host, int port, string key)
{
return _peer.SetIfPresent(_manager.Connect(host, port, key));
}

public void DropPeer()
{
_peer = _manager.Connect(host, port, key);
NetPeer? peer = _peer.Clear();
if (peer == null)
{
return;
}

_manager.DisconnectPeerForce(peer);
}

public void Send(byte[] data, DeliveryMethod deliveryMethod)
{
_peer?.Send(data, deliveryMethod);
_peer.Value?.Send(data, deliveryMethod);
}

public void PollEvents()
Expand All @@ -80,23 +110,37 @@ public void Stop()
_manager.Stop();
}

_peer = null;
_peer.Clear();
}

private void OnPeerConnected(NetPeer peer)
{
_peer = peer;
if (!_peer.IsCurrent(peer))
{
return;
}

PeerConnected?.Invoke();
}

private void OnPeerDisconnected(NetPeer peer, DisconnectInfo info)
{
_peer = null;
if (!_peer.TryClear(peer))
{
return;
}

PeerDisconnected?.Invoke(info.Reason.ToString());
}

private void OnNetworkReceive(NetPeer peer, NetPacketReader reader, byte channelNumber, DeliveryMethod deliveryMethod)
{
if (!_peer.IsCurrent(peer))
{
reader.Recycle();
return;
}

byte[] data = reader.GetRemainingBytes();
reader.Recycle();
NetworkReceive?.Invoke(data);
Expand All @@ -107,4 +151,48 @@ private void OnNetworkError(IPEndPoint endPoint, SocketError socketError)
NetworkError?.Invoke(endPoint, socketError);
}
}

internal sealed class CurrentPeerSlot<TPeer>
where TPeer : class
{
private TPeer? _value;

public TPeer? Value
{
get { return _value; }
}

public bool SetIfPresent(TPeer peer)
{
if (peer != null)
{
_value = peer;
}

return _value != null;
}

public TPeer? Clear()
{
TPeer? peer = _value;
_value = null;
return peer;
}

public bool IsCurrent(TPeer peer)
{
return ReferenceEquals(_value, peer);
}

public bool TryClear(TPeer peer)
{
if (!IsCurrent(peer))
{
return false;
}

_value = null;
return true;
}
}
}
Loading
Loading