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

orion-ecs / keen-eye / 20635738015

01 Jan 2026 08:52AM UTC coverage: 84.91% (-1.6%) from 86.483%
20635738015

push

github

tyevco
Add ViewportPanel with 3D scene rendering and transform gizmos (#588)

Implements the core viewport panel for the scene editor with:
- EditorCameraController with Orbit/Fly/TopDown camera modes
- Transform gizmos (translate/rotate/scale) with world/local space
- Ray-based entity picking and selection
- Preset camera views (Front, Back, Left, Right, Top, Bottom)
- Focus-on-selection functionality (F key)
- IInputProvider abstraction for polling-based input

Includes 56 unit tests for EditorCameraController and TransformGizmo.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

5012 of 5747 branches covered (87.21%)

Branch coverage included in aggregate %.

79 of 767 new or added lines in 5 files covered. (10.3%)

86 existing lines in 4 files now uncovered.

32138 of 38005 relevant lines covered (84.56%)

1.16 hits per line

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

59.15
/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
    // Pre-allocated list to avoid per-tick allocations
25
    private readonly List<(Entity entity, float priority, bool needsFullSync)> entitiesToUpdate = [];
1✔
26

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

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

46
        var serializer = plugin.Config.Serializer;
1✔
47
        var config = plugin.Config;
1✔
48

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

55
        // Collect entities that need updates and sort by priority
56
        entitiesToUpdate.Clear();
1✔
57

58
        foreach (var entity in World.Query<NetworkId, NetworkState>())
1✔
59
        {
60
            ref var networkState = ref World.Get<NetworkState>(entity);
1✔
61

62
            // Accumulate priority over time
63
            networkState.AccumulatedPriority += deltaTime;
1✔
64

65
            if (ShouldSendEntity(entity, ref networkState, serializer))
1✔
66
            {
67
                entitiesToUpdate.Add((entity, networkState.AccumulatedPriority, networkState.NeedsFullSync));
1✔
68
            }
69
        }
70

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

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

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

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

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

107
        // Pump the transport to flush outgoing data
108
        plugin.Transport.Update();
1✔
109
    }
1✔
110

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

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

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

132
        // Compare current state to last sent state using delta masks for efficiency
UNCOV
133
        foreach (var (type, value) in World.GetComponents(entity))
×
134
        {
UNCOV
135
            if (!serializer.IsNetworkSerializable(type))
×
136
            {
137
                continue;
138
            }
139

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

146
            // Use dirty mask for delta-supported types, fallback to Equals for others
UNCOV
147
            if (serializer.SupportsDelta(type))
×
148
            {
UNCOV
149
                if (serializer.GetDirtyMask(type, value, lastValue) != 0)
×
150
                {
151
                    return true;
×
152
                }
153
            }
UNCOV
154
            else if (!Equals(lastValue, value))
×
155
            {
UNCOV
156
                return true;
×
157
            }
158
        }
159

UNCOV
160
        return false;
×
UNCOV
161
    }
×
162

163
    private void SendEntityUpdate(Entity entity, NetworkId networkId, ref NetworkState state)
164
    {
165
        var writer = new NetworkMessageWriter(sendBuffer);
1✔
166
        var serializer = plugin.Config.Serializer;
1✔
167

168
        if (state.NeedsFullSync)
1✔
169
        {
170
            // Send full entity state
171
            writer.WriteHeader(MessageType.EntitySpawn, plugin.CurrentTick);
1✔
172

173
            var owner = World.Has<NetworkOwner>(entity)
1✔
174
                ? World.Get<NetworkOwner>(entity)
1✔
175
                : NetworkOwner.Server;
1✔
176

177
            writer.WriteEntitySpawn(networkId.Value, owner.ClientId);
1✔
178

179
            // Write all replicated components (full serialization)
180
            WriteReplicatedComponentsFull(entity, ref writer, serializer);
1✔
181

182
            state.NeedsFullSync = false;
1✔
183
        }
184
        else
185
        {
186
            // Send delta update (only changed fields within components)
UNCOV
187
            writer.WriteHeader(MessageType.ComponentDelta, plugin.CurrentTick);
×
188
            writer.WriteUInt32(networkId.Value);
×
189

190
            // Write components with delta encoding
UNCOV
191
            WriteReplicatedComponentsDelta(entity, ref writer, serializer);
×
192
        }
193

194
        var span = writer.GetWrittenSpan();
1✔
195
        bytesSentThisTick += span.Length;
1✔
196
        plugin.SendToAll(span, DeliveryMode.UnreliableSequenced);
1✔
197

198
        // Update last sent state for delta tracking
199
        SaveSentState(entity, serializer);
1✔
200
    }
1✔
201

202
    private void WriteReplicatedComponentsFull(Entity entity, ref NetworkMessageWriter writer, INetworkSerializer? serializer)
203
    {
204
        if (serializer is null)
1✔
205
        {
206
            writer.WriteComponentCount(0);
1✔
207
            return;
1✔
208
        }
209

210
        // Collect all replicated components
211
        var toSend = new List<(Type, object)>();
1✔
212
        foreach (var (type, value) in World.GetComponents(entity))
1✔
213
        {
214
            if (serializer.IsNetworkSerializable(type))
1✔
215
            {
UNCOV
216
                toSend.Add((type, value));
×
217
            }
218
        }
219

220
        writer.WriteComponentCount((byte)toSend.Count);
1✔
221
        foreach (var (type, value) in toSend)
1✔
222
        {
UNCOV
223
            writer.WriteComponent(serializer, type, value);
×
224
        }
225
    }
1✔
226

227
    private void WriteReplicatedComponentsDelta(Entity entity, ref NetworkMessageWriter writer, INetworkSerializer? serializer)
228
    {
229
        if (serializer is null)
×
230
        {
UNCOV
231
            writer.WriteComponentCount(0);
×
UNCOV
232
            return;
×
233
        }
234

235
        // Get last sent state for delta comparison
236
        lastSentState.TryGetValue(entity, out var entityLastState);
×
237

238
        // Collect components that have changed
239
        var toSend = new List<(Type type, object current, object? baseline)>();
×
UNCOV
240
        foreach (var (type, value) in World.GetComponents(entity))
×
241
        {
UNCOV
242
            if (!serializer.IsNetworkSerializable(type))
×
243
            {
244
                continue;
245
            }
246

247
            object? lastValue = null;
×
UNCOV
248
            if (entityLastState is not null)
×
249
            {
UNCOV
250
                entityLastState.TryGetValue(type, out lastValue);
×
251
            }
252

253
            // Check if changed using delta mask or equality
254
            bool hasChanged;
UNCOV
255
            if (lastValue is null)
×
256
            {
UNCOV
257
                hasChanged = true; // New component
×
258
            }
UNCOV
259
            else if (serializer.SupportsDelta(type))
×
260
            {
UNCOV
261
                hasChanged = serializer.GetDirtyMask(type, value, lastValue) != 0;
×
262
            }
263
            else
264
            {
265
                hasChanged = !Equals(lastValue, value);
×
266
            }
267

UNCOV
268
            if (hasChanged)
×
269
            {
UNCOV
270
                toSend.Add((type, value, lastValue));
×
271
            }
272
        }
273

274
        writer.WriteComponentCount((byte)toSend.Count);
×
275

276
        // Write each component with delta encoding where supported
277
        foreach (var (type, current, baseline) in toSend)
×
278
        {
279
            // Use delta serialization if we have a baseline and the type supports it
UNCOV
280
            if (baseline is not null && serializer.SupportsDelta(type))
×
281
            {
UNCOV
282
                writer.WriteComponentDelta(serializer, type, current, baseline);
×
283
            }
284
            else
285
            {
286
                // Fall back to full serialization
287
                writer.WriteComponent(serializer, type, current);
×
288
            }
289
        }
UNCOV
290
    }
×
291

292
    private void SaveSentState(Entity entity, INetworkSerializer? serializer)
293
    {
294
        if (serializer is null)
1✔
295
        {
296
            return;
1✔
297
        }
298

299
        if (!lastSentState.TryGetValue(entity, out var entityState))
1✔
300
        {
301
            entityState = [];
1✔
302
            lastSentState[entity] = entityState;
1✔
303
        }
304

305
        // Save current state of all replicated components
306
        foreach (var (type, value) in World.GetComponents(entity))
1✔
307
        {
308
            if (serializer.IsNetworkSerializable(type))
1✔
309
            {
310
                // Store a copy of the value (boxing creates a copy for value types)
UNCOV
311
                entityState[type] = value;
×
312
            }
313
        }
314
    }
1✔
315

316
    /// <summary>
317
    /// Clears tracking state for an entity (call when entity is despawned).
318
    /// </summary>
319
    /// <param name="entity">The entity to clear.</param>
320
    public void ClearEntityState(Entity entity)
321
    {
UNCOV
322
        lastSentState.Remove(entity);
×
UNCOV
323
    }
×
324
}
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