• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

orion-ecs / keen-eye / 20585547123

30 Dec 2025 12:10AM UTC coverage: 69.855% (+2.8%) from 67.047%
20585547123

push

github

tyevco
Implement RTT measurement, ownership transfer, and input processing

Client-side:
- Add RTT tracking with SendPing() and RoundTripTimeMs property
- Handle Pong messages to calculate round-trip time
- Implement ownership transfer handling with tag swapping
- Add OwnershipChanged event for ownership notifications
- Handle ConnectionRejected with reason codes

Server-side:
- Add ClientInputReceived event for processing client inputs
- Pass raw input data to event listeners for game-specific handling

Protocol:
- Add ReadByte/WriteByte helpers for reason codes and small values

This completes all the networking plugin TODOs for Issue #353.

3492 of 4450 branches covered (78.47%)

Branch coverage included in aggregate %.

8 of 61 new or added lines in 2 files covered. (13.11%)

374 existing lines in 13 files now uncovered.

20184 of 29443 relevant lines covered (68.55%)

1.03 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

57.25
/src/KeenEyes.Network/Systems/NetworkServerSendSystem.cs
1
using KeenEyes.Network.Components;
2
using KeenEyes.Network.Protocol;
3
using KeenEyes.Network.Serialization;
4
using KeenEyes.Network.Transport;
5

6
namespace KeenEyes.Network.Systems;
7

8
/// <summary>
9
/// Server system that sends state updates to clients.
10
/// </summary>
11
/// <remarks>
12
/// Runs in LateUpdate phase after game logic has executed.
13
/// </remarks>
14
public sealed class NetworkServerSendSystem(NetworkServerPlugin plugin) : SystemBase
1✔
15
{
16
    private readonly byte[] sendBuffer = new byte[4096];
1✔
17

18
    // Track last sent component values per entity for delta detection
19
    private readonly Dictionary<Entity, Dictionary<Type, object>> lastSentState = [];
1✔
20

21
    // Track bytes sent this tick for bandwidth limiting
22
    private int bytesSentThisTick;
23

24
    /// <inheritdoc/>
25
    public override void Update(float deltaTime)
26
    {
27
        // Advance tick
28
        if (!plugin.Tick(deltaTime))
1✔
29
        {
30
            return; // Not time for a network tick yet
1✔
31
        }
32

33
        // Check for clients that need full snapshots
34
        foreach (var client in plugin.GetConnectedClients())
1✔
35
        {
36
            if (client.NeedsFullSnapshot)
1✔
37
            {
38
                plugin.SendFullSnapshot(client.ClientId);
1✔
39
                client.NeedsFullSnapshot = false;
1✔
40
            }
41
        }
42

43
        var serializer = plugin.Config.Serializer;
1✔
44
        var config = plugin.Config;
1✔
45

46
        // Calculate bytes per tick budget
47
        var bytesPerTick = config.EnableBandwidthLimiting
1✔
48
            ? config.MaxBandwidthBytesPerSecond / config.TickRate
1✔
49
            : int.MaxValue;
1✔
50
        bytesSentThisTick = 0;
1✔
51

52
        // Collect entities that need updates and sort by priority
53
        var entitiesToUpdate = new List<(Entity entity, float priority, bool needsFullSync)>();
1✔
54

55
        foreach (var entity in World.Query<NetworkId, NetworkState>())
1✔
56
        {
57
            ref var networkState = ref World.Get<NetworkState>(entity);
1✔
58

59
            // Accumulate priority over time
60
            networkState.AccumulatedPriority += deltaTime;
1✔
61

62
            if (ShouldSendEntity(entity, ref networkState, serializer))
1✔
63
            {
64
                entitiesToUpdate.Add((entity, networkState.AccumulatedPriority, networkState.NeedsFullSync));
1✔
65
            }
66
        }
67

68
        // Sort by priority (higher first), with full sync entities always at front
69
        entitiesToUpdate.Sort((a, b) =>
1✔
70
        {
1✔
71
            // Full sync entities have highest priority
1✔
UNCOV
72
            if (a.needsFullSync != b.needsFullSync)
×
73
            {
1✔
UNCOV
74
                return a.needsFullSync ? -1 : 1;
×
75
            }
1✔
UNCOV
76
            return b.priority.CompareTo(a.priority);
×
77
        });
1✔
78

79
        // Send entity updates within bandwidth budget
80
        foreach (var (entity, _, _) in entitiesToUpdate)
1✔
81
        {
82
            ref var networkState = ref World.Get<NetworkState>(entity);
1✔
83
            ref readonly var networkId = ref World.Get<NetworkId>(entity);
1✔
84

85
            // Check bandwidth budget
86
            if (config.EnableBandwidthLimiting && bytesSentThisTick >= bytesPerTick)
1✔
87
            {
88
                // Don't reset priority for entities we couldn't send
UNCOV
89
                break;
×
90
            }
91

92
            var bytesBefore = bytesSentThisTick;
1✔
93
            SendEntityUpdate(entity, networkId, ref networkState);
1✔
94
            networkState.LastSentTick = plugin.CurrentTick;
1✔
95
            networkState.AccumulatedPriority = 0; // Reset priority after sending
1✔
96

97
            // Stop if we've exceeded budget (but we already sent this message)
98
            if (config.EnableBandwidthLimiting && bytesSentThisTick > bytesPerTick)
1✔
99
            {
UNCOV
100
                break;
×
101
            }
102
        }
103

104
        // Pump the transport to flush outgoing data
105
        plugin.Transport.Update();
1✔
106
    }
1✔
107

108
    private bool ShouldSendEntity(Entity entity, ref NetworkState state, INetworkSerializer? serializer)
109
    {
110
        // Always send if needs full sync
111
        if (state.NeedsFullSync)
1✔
112
        {
113
            return true;
1✔
114
        }
115

116
        // If no serializer, we can only send spawn/despawn
UNCOV
117
        if (serializer is null)
×
118
        {
UNCOV
119
            return false;
×
120
        }
121

122
        // Check if any replicated component has changed
UNCOV
123
        if (!lastSentState.TryGetValue(entity, out var entityState))
×
124
        {
125
            // Never sent this entity - needs update
UNCOV
126
            return true;
×
127
        }
128

129
        // Compare current state to last sent state
UNCOV
130
        foreach (var (type, value) in World.GetComponents(entity))
×
131
        {
UNCOV
132
            if (!serializer.IsNetworkSerializable(type))
×
133
            {
134
                continue;
135
            }
136

UNCOV
137
            if (!entityState.TryGetValue(type, out var lastValue))
×
138
            {
139
                // New component - needs update
UNCOV
140
                return true;
×
141
            }
142

143
            // Compare values (simple equality check)
UNCOV
144
            if (!Equals(lastValue, value))
×
145
            {
UNCOV
146
                return true;
×
147
            }
148
        }
149

UNCOV
150
        return false;
×
UNCOV
151
    }
×
152

153
    private void SendEntityUpdate(Entity entity, NetworkId networkId, ref NetworkState state)
154
    {
155
        var writer = new NetworkMessageWriter(sendBuffer);
1✔
156
        var serializer = plugin.Config.Serializer;
1✔
157

158
        if (state.NeedsFullSync)
1✔
159
        {
160
            // Send full entity state
161
            writer.WriteHeader(MessageType.EntitySpawn, plugin.CurrentTick);
1✔
162

163
            var owner = World.Has<NetworkOwner>(entity)
1✔
164
                ? World.Get<NetworkOwner>(entity)
1✔
165
                : NetworkOwner.Server;
1✔
166

167
            writer.WriteEntitySpawn(networkId.Value, owner.ClientId);
1✔
168

169
            // Write all replicated components
170
            WriteReplicatedComponents(entity, ref writer, serializer, isDelta: false);
1✔
171

172
            state.NeedsFullSync = false;
1✔
173
        }
174
        else
175
        {
176
            // Send delta update (only changed components)
UNCOV
177
            writer.WriteHeader(MessageType.ComponentUpdate, plugin.CurrentTick);
×
UNCOV
178
            writer.WriteUInt32(networkId.Value);
×
179

180
            // Write only changed components
UNCOV
181
            WriteReplicatedComponents(entity, ref writer, serializer, isDelta: true);
×
182
        }
183

184
        var span = writer.GetWrittenSpan();
1✔
185
        bytesSentThisTick += span.Length;
1✔
186
        plugin.SendToAll(span, DeliveryMode.UnreliableSequenced);
1✔
187

188
        // Update last sent state for delta tracking
189
        SaveSentState(entity, serializer);
1✔
190
    }
1✔
191

192
    private void WriteReplicatedComponents(Entity entity, ref NetworkMessageWriter writer, INetworkSerializer? serializer, bool isDelta)
193
    {
194
        if (serializer is null)
1✔
195
        {
196
            // No serializer configured, write 0 components
197
            writer.WriteComponentCount(0);
1✔
198
            return;
1✔
199
        }
200

201
        // Get last sent state for delta comparison
UNCOV
202
        lastSentState.TryGetValue(entity, out var entityLastState);
×
203

204
        // Collect components to send
UNCOV
205
        var toSend = new List<(Type, object)>();
×
UNCOV
206
        foreach (var (type, value) in World.GetComponents(entity))
×
207
        {
UNCOV
208
            if (!serializer.IsNetworkSerializable(type))
×
209
            {
210
                continue;
211
            }
212

213
            // For delta updates, only send changed components
UNCOV
214
            if (isDelta && entityLastState is not null)
×
215
            {
UNCOV
216
                if (entityLastState.TryGetValue(type, out var lastValue) && Equals(lastValue, value))
×
217
                {
218
                    continue; // Component unchanged
219
                }
220
            }
221

UNCOV
222
            toSend.Add((type, value));
×
223
        }
224

UNCOV
225
        writer.WriteComponentCount((byte)toSend.Count);
×
226

227
        // Write each component
UNCOV
228
        foreach (var (type, value) in toSend)
×
229
        {
UNCOV
230
            writer.WriteComponent(serializer, type, value);
×
231
        }
UNCOV
232
    }
×
233

234
    private void SaveSentState(Entity entity, INetworkSerializer? serializer)
235
    {
236
        if (serializer is null)
1✔
237
        {
238
            return;
1✔
239
        }
240

UNCOV
241
        if (!lastSentState.TryGetValue(entity, out var entityState))
×
242
        {
UNCOV
243
            entityState = [];
×
UNCOV
244
            lastSentState[entity] = entityState;
×
245
        }
246

247
        // Save current state of all replicated components
UNCOV
248
        foreach (var (type, value) in World.GetComponents(entity))
×
249
        {
UNCOV
250
            if (serializer.IsNetworkSerializable(type))
×
251
            {
252
                // Store a copy of the value (boxing creates a copy for value types)
UNCOV
253
                entityState[type] = value;
×
254
            }
255
        }
UNCOV
256
    }
×
257

258
    /// <summary>
259
    /// Clears tracking state for an entity (call when entity is despawned).
260
    /// </summary>
261
    /// <param name="entity">The entity to clear.</param>
262
    public void ClearEntityState(Entity entity)
263
    {
UNCOV
264
        lastSentState.Remove(entity);
×
UNCOV
265
    }
×
266
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc