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

orion-ecs / keen-eye / 29923911837

22 Jul 2026 01:26PM UTC coverage: 62.597% (-2.6%) from 65.151%
29923911837

push

github

tyevco
test(mcp): Update TestBridge mocks to current interface surface

MockTestBridge and MockCaptureController never implemented the members added
during the MCP Tools Expansion, so tests/KeenEyes.Mcp.TestBridge.Tests did not
compile on main. MockStateController had also drifted (GetComponentAsync and
EntitySnapshot.Components now use JsonElement).

- MockTestBridge: add Window, Time, Systems, Mutation, Profile, Snapshot, AI,
  Replay and InputContext, backed by new hand-written mock controllers plus
  KeenEyes.Testing's MockInputContext, mirroring the existing recording /
  canned-result mock style.
- MockCaptureController: add CaptureRegionAsync, GetRegionScreenshotBytesAsync
  and SaveRegionScreenshotAsync.
- MockStateController: return JsonElement from GetComponentAsync and store
  component data as JsonElement to match the current IStateController.
- Rename pre-existing PascalCase private fields in InputParsingTests to
  camelCase so the project passes dotnet format once it is in CI.

The project was MISSING from KeenEyes.slnx, which is why the compile break went
unnoticed (CI builds the solution). Adding it to the solution is itself the
cheap drift guard requested in the issue: CI now compiles the mocks against the
interfaces, so any future interface addition breaks the build immediately. All
80 tests pass under --max-parallel-test-modules 1.

Fixes #1025

8144 of 12399 branches covered (65.68%)

Branch coverage included in aggregate %.

48975 of 78850 relevant lines covered (62.11%)

0.95 hits per line

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

80.2
/src/KeenEyes.Network/Systems/NetworkClientSendSystem.cs
1
using KeenEyes.Capabilities;
2
using KeenEyes.Network.Components;
3
using KeenEyes.Network.Prediction;
4
using KeenEyes.Network.Protocol;
5
using KeenEyes.Network.Replication;
6
using KeenEyes.Network.Serialization;
7
using KeenEyes.Network.Transport;
8

9
namespace KeenEyes.Network.Systems;
10

11
/// <summary>
12
/// Client system that sends input to the server.
13
/// </summary>
14
/// <remarks>
15
/// Runs in LateUpdate phase after local prediction.
16
/// </remarks>
17
public sealed class NetworkClientSendSystem(NetworkClientPlugin plugin) : SystemBase
1✔
18
{
19
    private readonly byte[] sendBuffer = new byte[1024];
1✔
20
    private float pingTimer;
21
    private const float PingInterval = 1.0f; // Send ping every second
22
    private uint lastSentInputTick;
23

24
    // Owner-authoritative state tracking.
25
    private float ownerStateAccumulator;
26
    private OwnerAuthoritativeComponentSet? ownerAuthTypes;
27
    private INetworkSerializer? ownerAuthTypesSource;
28

29
    // Track last sent owner-authoritative component values per entity for dirty detection.
30
    private readonly Dictionary<Entity, Dictionary<Type, object>> lastSentOwnerState = [];
1✔
31

32
    /// <inheritdoc/>
33
    public override void Update(float deltaTime)
34
    {
35
        if (!plugin.IsConnected)
1✔
36
        {
37
            return;
1✔
38
        }
39

40
        // Send periodic ping for latency measurement
41
        pingTimer += deltaTime;
1✔
42
        if (pingTimer >= PingInterval)
1✔
43
        {
44
            pingTimer -= PingInterval;
1✔
45
            SendPing();
1✔
46
        }
47

48
        // Send input for predicted entities
49
        if (plugin.Config.EnablePrediction)
1✔
50
        {
51
            SendInput();
1✔
52
        }
53

54
        // Send owner-authoritative component state at the network tick cadence.
55
        SendOwnerState(deltaTime);
1✔
56

57
        // Pump the transport to flush outgoing data
58
        plugin.Transport.Update();
1✔
59
    }
1✔
60

61
    private void SendPing()
62
    {
63
        var writer = new NetworkMessageWriter(sendBuffer);
1✔
64
        writer.WriteHeader(MessageType.Ping, plugin.CurrentTick);
1✔
65
        plugin.SendToServer(writer.GetWrittenSpan(), DeliveryMode.Unreliable);
1✔
66
    }
1✔
67

68
    private void SendInput()
69
    {
70
        var inputSerializer = plugin.Config.InputSerializer;
1✔
71
        if (inputSerializer is null)
1✔
72
        {
73
            return;
1✔
74
        }
75

76
        // Find locally owned predicted entities and send their input
77
        foreach (var entity in World.Query<LocallyOwned, Predicted, NetworkId>())
×
78
        {
79
            var inputBuffer = plugin.GetInputBuffer(entity) as IInputBuffer;
×
80
            if (inputBuffer is null || inputBuffer.NewestTick <= lastSentInputTick)
×
81
            {
82
                continue;
83
            }
84

85
            ref readonly var networkId = ref World.Get<NetworkId>(entity);
×
86

87
            // Send all unsent inputs (redundantly for reliability)
88
            // We send the last few inputs to handle packet loss
89
            var startTick = lastSentInputTick > 0 ? lastSentInputTick : inputBuffer.OldestTick;
×
90

91
            foreach (var input in inputBuffer.GetInputsFromBoxed(startTick))
×
92
            {
93
                var writer = new NetworkMessageWriter(sendBuffer);
×
94
                writer.WriteHeader(MessageType.ClientInput, plugin.CurrentTick);
×
95
                writer.WriteNetworkId(networkId.Value);
×
96
                writer.WriteInput(inputSerializer, input);
×
97

98
                plugin.SendToServer(writer.GetWrittenSpan(), DeliveryMode.UnreliableSequenced);
×
99
            }
100

101
            lastSentInputTick = inputBuffer.NewestTick;
×
102
        }
103
    }
×
104

105
    private void SendOwnerState(float deltaTime)
106
    {
107
        var serializer = plugin.Config.Serializer;
1✔
108
        if (serializer is null)
1✔
109
        {
110
            return;
1✔
111
        }
112

113
        // Gate sends to the configured network tick cadence.
114
        var tickInterval = 1f / plugin.Config.TickRate;
1✔
115
        ownerStateAccumulator += deltaTime;
1✔
116
        if (ownerStateAccumulator < tickInterval)
1✔
117
        {
118
            return;
×
119
        }
120

121
        ownerStateAccumulator -= tickInterval;
1✔
122

123
        var ownerAuth = GetOwnerAuthTypes(serializer);
1✔
124
        if (World is not ISnapshotCapability snapshot)
1✔
125
        {
126
            return;
×
127
        }
128

129
        // Send owner-authoritative state for entities this client owns.
130
        foreach (var entity in World.Query<LocallyOwned, NetworkId>())
1✔
131
        {
132
            ref readonly var networkId = ref World.Get<NetworkId>(entity);
1✔
133
            SendEntityOwnerState(entity, networkId, snapshot, serializer, ownerAuth);
1✔
134
        }
135
    }
1✔
136

137
    private void SendEntityOwnerState(
138
        Entity entity,
139
        NetworkId networkId,
140
        ISnapshotCapability snapshot,
141
        INetworkSerializer serializer,
142
        OwnerAuthoritativeComponentSet ownerAuth)
143
    {
144
        // Collect owner-authoritative components whose value changed since the last send.
145
        lastSentOwnerState.TryGetValue(entity, out var entityLastState);
1✔
146

147
        var toSend = new List<(Type type, object value)>();
1✔
148
        foreach (var (type, value) in snapshot.GetComponents(entity))
1✔
149
        {
150
            if (!ownerAuth.Contains(type) || !serializer.IsNetworkSerializable(type))
1✔
151
            {
152
                continue;
153
            }
154

155
            if (!HasChanged(serializer, type, value, entityLastState))
1✔
156
            {
157
                continue;
158
            }
159

160
            toSend.Add((type, value));
1✔
161
        }
162

163
        if (toSend.Count == 0)
1✔
164
        {
165
            return;
1✔
166
        }
167

168
        var writer = new NetworkMessageWriter(sendBuffer);
1✔
169
        writer.WriteHeader(MessageType.OwnerStateUpdate, plugin.CurrentTick);
1✔
170
        writer.WriteNetworkId(networkId.Value);
1✔
171
        writer.WriteComponentCount((byte)toSend.Count);
1✔
172
        foreach (var (type, value) in toSend)
1✔
173
        {
174
            writer.WriteComponent(serializer, type, value);
1✔
175
        }
176

177
        plugin.SendToServer(writer.GetWrittenSpan(), DeliveryMode.UnreliableSequenced);
1✔
178

179
        // Record the sent state so unchanged components are not re-sent next tick.
180
        if (entityLastState is null)
1✔
181
        {
182
            entityLastState = [];
1✔
183
            lastSentOwnerState[entity] = entityLastState;
1✔
184
        }
185

186
        foreach (var (type, value) in toSend)
1✔
187
        {
188
            entityLastState[type] = value;
1✔
189
        }
190
    }
1✔
191

192
    private static bool HasChanged(
193
        INetworkSerializer serializer,
194
        Type type,
195
        object value,
196
        Dictionary<Type, object>? entityLastState)
197
    {
198
        if (entityLastState is null || !entityLastState.TryGetValue(type, out var lastValue))
1✔
199
        {
200
            return true;
1✔
201
        }
202

203
        if (serializer.SupportsDelta(type))
1✔
204
        {
205
            return serializer.GetDirtyMask(type, value, lastValue) != 0;
×
206
        }
207

208
        return !Equals(lastValue, value);
1✔
209
    }
210

211
    private OwnerAuthoritativeComponentSet GetOwnerAuthTypes(INetworkSerializer serializer)
212
    {
213
        if (ownerAuthTypes is null || !ReferenceEquals(ownerAuthTypesSource, serializer))
1✔
214
        {
215
            ownerAuthTypes = new OwnerAuthoritativeComponentSet(serializer);
1✔
216
            ownerAuthTypesSource = serializer;
1✔
217
        }
218

219
        return ownerAuthTypes;
1✔
220
    }
221
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc