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

orion-ecs / keen-eye / 30113831081

24 Jul 2026 05:39PM UTC coverage: 64.224% (-0.07%) from 64.291%
30113831081

push

github

tyevco
fix(audio): Fix non-looping replay, WAV validation, paused-source recycling, double master volume (#1185,#1186,#1188,#1189)

- #1185: AudioSourceSystem now restarts a stopped backend source when State is set back to Playing (distinguishes a replay request from a natural finish via CurrentSound validity), so non-looping sources are replayable per the AudioSource.State contract.
- #1186: WavDecoder validates each chunk size against the remaining bytes and throws AudioLoadException on negative/oversized sizes, preventing the ArgumentOutOfRangeException slice crash and the negative-size infinite loop on untrusted files.
- #1188: SourcePool.Update only recycles Stopped sources (Paused treated as still-active), so Pause/PauseAll no longer kills pooled one-shots on the next frame.
- #1189: SilkAudioContext applies master + Master-channel volume only via the listener gain; ComputeEffectiveVolume no longer re-multiplies them into the per-source gain, eliminating the squared master volume and the new-vs-playing inconsistency.

Adds KeenEyes.Audio.Tests (AudioSourceSystem replay/finish, via fake IAudioDevice/IAudioContext) and KeenEyes.Audio.Silk.Tests (WavDecoder malformed-input validation). #1188/#1189 are OpenAL-device-bound and verified by code trace.

Closes #1185
Closes #1186
Closes #1188
Closes #1189

8612 of 12731 branches covered (67.65%)

Branch coverage included in aggregate %.

10 of 15 new or added lines in 4 files covered. (66.67%)

368 existing lines in 26 files now uncovered.

51312 of 80574 relevant lines covered (63.68%)

0.99 hits per line

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

97.14
/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

23
    // High-water mark of the newest input tick already sent, tracked per predicted
24
    // entity. A single shared field skipped every predicted entity after the first one
25
    // reached a given tick, so only one entity's input ever reached the server (#1100).
26
    private readonly Dictionary<Entity, uint> lastSentInputTicks = [];
1✔
27

28
    // Owner-authoritative state tracking.
29
    private float ownerStateAccumulator;
30
    private OwnerAuthoritativeComponentSet? ownerAuthTypes;
31
    private INetworkSerializer? ownerAuthTypesSource;
32

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

36
    /// <inheritdoc/>
37
    public override void Update(float deltaTime)
38
    {
39
        if (!plugin.IsConnected)
1✔
40
        {
41
            return;
1✔
42
        }
43

44
        // Send periodic ping for latency measurement
45
        pingTimer += deltaTime;
1✔
46
        if (pingTimer >= PingInterval)
1✔
47
        {
48
            pingTimer -= PingInterval;
1✔
49
            SendPing();
1✔
50
        }
51

52
        // Send input for predicted entities
53
        if (plugin.Config.EnablePrediction)
1✔
54
        {
55
            SendInput();
1✔
56
        }
57

58
        // Send owner-authoritative component state at the network tick cadence.
59
        SendOwnerState(deltaTime);
1✔
60

61
        // Pump the transport to flush outgoing data
62
        plugin.Transport.Update();
1✔
63
    }
1✔
64

65
    private void SendPing()
66
    {
67
        var writer = new NetworkMessageWriter(sendBuffer);
1✔
68
        writer.WriteHeader(MessageType.Ping, plugin.CurrentTick);
1✔
69
        plugin.SendToServer(writer.GetWrittenSpan(), DeliveryMode.Unreliable);
1✔
70
    }
1✔
71

72
    private void SendInput()
73
    {
74
        var inputSerializer = plugin.Config.InputSerializer;
1✔
75
        if (inputSerializer is null)
1✔
76
        {
77
            return;
1✔
78
        }
79

80
        // Find locally owned predicted entities and send their input
81
        foreach (var entity in World.Query<LocallyOwned, Predicted, NetworkId>())
1✔
82
        {
83
            var inputBuffer = plugin.GetInputBuffer(entity) as IInputBuffer;
1✔
84
            if (inputBuffer is null)
1✔
85
            {
86
                continue;
87
            }
88

89
            // Per-entity high-water mark: each predicted entity advances independently
90
            // so that two entities recording input for the same tick both send (#1100).
91
            lastSentInputTicks.TryGetValue(entity, out var lastSentInputTick);
1✔
92
            if (inputBuffer.NewestTick <= lastSentInputTick)
1✔
93
            {
94
                continue;
95
            }
96

97
            ref readonly var networkId = ref World.Get<NetworkId>(entity);
1✔
98

99
            // Send all unsent inputs (redundantly for reliability)
100
            // We send the last few inputs to handle packet loss
101
            var startTick = lastSentInputTick > 0 ? lastSentInputTick : inputBuffer.OldestTick;
1✔
102

103
            foreach (var input in inputBuffer.GetInputsFromBoxed(startTick))
1✔
104
            {
105
                var writer = new NetworkMessageWriter(sendBuffer);
1✔
106
                writer.WriteHeader(MessageType.ClientInput, plugin.CurrentTick);
1✔
107
                writer.WriteNetworkId(networkId.Value);
1✔
108
                writer.WriteInput(inputSerializer, input);
1✔
109

110
                plugin.SendToServer(writer.GetWrittenSpan(), DeliveryMode.UnreliableSequenced);
1✔
111
            }
112

113
            lastSentInputTicks[entity] = inputBuffer.NewestTick;
1✔
114
        }
115
    }
1✔
116

117
    private void SendOwnerState(float deltaTime)
118
    {
119
        var serializer = plugin.Config.Serializer;
1✔
120
        if (serializer is null)
1✔
121
        {
122
            return;
1✔
123
        }
124

125
        // Gate sends to the configured network tick cadence.
126
        var tickInterval = 1f / plugin.Config.TickRate;
1✔
127
        ownerStateAccumulator += deltaTime;
1✔
128
        if (ownerStateAccumulator < tickInterval)
1✔
129
        {
UNCOV
130
            return;
×
131
        }
132

133
        ownerStateAccumulator -= tickInterval;
1✔
134

135
        var ownerAuth = GetOwnerAuthTypes(serializer);
1✔
136
        if (World is not ISnapshotCapability snapshot)
1✔
137
        {
UNCOV
138
            return;
×
139
        }
140

141
        // Send owner-authoritative state for entities this client owns.
142
        foreach (var entity in World.Query<LocallyOwned, NetworkId>())
1✔
143
        {
144
            ref readonly var networkId = ref World.Get<NetworkId>(entity);
1✔
145
            SendEntityOwnerState(entity, networkId, snapshot, serializer, ownerAuth);
1✔
146
        }
147
    }
1✔
148

149
    private void SendEntityOwnerState(
150
        Entity entity,
151
        NetworkId networkId,
152
        ISnapshotCapability snapshot,
153
        INetworkSerializer serializer,
154
        OwnerAuthoritativeComponentSet ownerAuth)
155
    {
156
        // Collect owner-authoritative components whose value changed since the last send.
157
        lastSentOwnerState.TryGetValue(entity, out var entityLastState);
1✔
158

159
        var toSend = new List<(Type type, object value)>();
1✔
160
        foreach (var (type, value) in snapshot.GetComponents(entity))
1✔
161
        {
162
            if (!ownerAuth.Contains(type) || !serializer.IsNetworkSerializable(type))
1✔
163
            {
164
                continue;
165
            }
166

167
            if (!HasChanged(serializer, type, value, entityLastState))
1✔
168
            {
169
                continue;
170
            }
171

172
            toSend.Add((type, value));
1✔
173
        }
174

175
        if (toSend.Count == 0)
1✔
176
        {
177
            return;
1✔
178
        }
179

180
        var writer = new NetworkMessageWriter(sendBuffer);
1✔
181
        writer.WriteHeader(MessageType.OwnerStateUpdate, plugin.CurrentTick);
1✔
182
        writer.WriteNetworkId(networkId.Value);
1✔
183
        writer.WriteComponentCount((byte)toSend.Count);
1✔
184
        foreach (var (type, value) in toSend)
1✔
185
        {
186
            writer.WriteComponent(serializer, type, value);
1✔
187
        }
188

189
        plugin.SendToServer(writer.GetWrittenSpan(), DeliveryMode.UnreliableSequenced);
1✔
190

191
        // Record the sent state so unchanged components are not re-sent next tick.
192
        if (entityLastState is null)
1✔
193
        {
194
            entityLastState = [];
1✔
195
            lastSentOwnerState[entity] = entityLastState;
1✔
196
        }
197

198
        foreach (var (type, value) in toSend)
1✔
199
        {
200
            entityLastState[type] = value;
1✔
201
        }
202
    }
1✔
203

204
    private static bool HasChanged(
205
        INetworkSerializer serializer,
206
        Type type,
207
        object value,
208
        Dictionary<Type, object>? entityLastState)
209
    {
210
        if (entityLastState is null || !entityLastState.TryGetValue(type, out var lastValue))
1✔
211
        {
212
            return true;
1✔
213
        }
214

215
        if (serializer.SupportsDelta(type))
1✔
216
        {
UNCOV
217
            return serializer.GetDirtyMask(type, value, lastValue) != 0;
×
218
        }
219

220
        return !Equals(lastValue, value);
1✔
221
    }
222

223
    private OwnerAuthoritativeComponentSet GetOwnerAuthTypes(INetworkSerializer serializer)
224
    {
225
        if (ownerAuthTypes is null || !ReferenceEquals(ownerAuthTypesSource, serializer))
1✔
226
        {
227
            ownerAuthTypes = new OwnerAuthoritativeComponentSet(serializer);
1✔
228
            ownerAuthTypesSource = serializer;
1✔
229
        }
230

231
        return ownerAuthTypes;
1✔
232
    }
233
}
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