Skip to content

Commit e292e70

Browse files
committed
BodyAnimation Coalescer
1 parent 8b16f9a commit e292e70

File tree

3 files changed

+106
-2
lines changed

3 files changed

+106
-2
lines changed
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
using System.Collections.Concurrent;
2+
3+
using Chaos.Networking.Entities.Server;
4+
5+
namespace Darkages.Network.Client.Coalescer;
6+
7+
public sealed class BodyAnimationCoalescer : IDisposable
8+
{
9+
private readonly Action<BodyAnimationArgs> _sendImmediate;
10+
private readonly int _windowMs;
11+
private readonly int _maxPerWindow;
12+
13+
// Latest-wins per SourceId
14+
private readonly ConcurrentDictionary<uint, BodyAnimationArgs> _pending = new();
15+
16+
private int _flushScheduled; // 0/1
17+
private CancellationTokenSource? _cts;
18+
19+
public BodyAnimationCoalescer(Action<BodyAnimationArgs>? sendImmediate, int windowMs = 50, int maxPerWindow = 32)
20+
{
21+
// Default no-op
22+
sendImmediate ??= static _ => { };
23+
24+
_sendImmediate = sendImmediate;
25+
_windowMs = windowMs <= 0 ? 30 : windowMs;
26+
_maxPerWindow = maxPerWindow <= 0 ? 64 : maxPerWindow;
27+
}
28+
29+
public void Enqueue(BodyAnimationArgs args)
30+
{
31+
// Dedupe: if the newest is identical to what we already have pending for this SourceId, kick out
32+
if (_pending.TryGetValue(args.SourceId, out var existing) && AreEquivalent(existing, args))
33+
return;
34+
35+
// Latest wins for this SourceId
36+
_pending[args.SourceId] = args;
37+
38+
// Schedule one flush task per window
39+
if (Interlocked.Exchange(ref _flushScheduled, 1) == 0)
40+
{
41+
_cts ??= new CancellationTokenSource();
42+
_ = FlushAfterDelayAsync(_cts.Token);
43+
}
44+
}
45+
46+
private static bool AreEquivalent(BodyAnimationArgs a, BodyAnimationArgs b)
47+
=> a.BodyAnimation == b.BodyAnimation
48+
&& a.AnimationSpeed == b.AnimationSpeed
49+
&& a.Sound == b.Sound;
50+
51+
private async Task FlushAfterDelayAsync(CancellationToken ct)
52+
{
53+
try
54+
{
55+
await Task.Delay(_windowMs, ct).ConfigureAwait(false);
56+
57+
// Send at most _maxPerWindow unique SourceIds this tick
58+
var sent = 0;
59+
60+
foreach (var kv in _pending)
61+
{
62+
if (sent >= _maxPerWindow)
63+
break;
64+
65+
if (_pending.TryRemove(kv.Key, out var args))
66+
{
67+
_sendImmediate(args);
68+
sent++;
69+
}
70+
}
71+
72+
// Hard cap protection: drop any remaining this window
73+
if (!_pending.IsEmpty)
74+
_pending.Clear();
75+
}
76+
catch (OperationCanceledException) { }
77+
finally
78+
{
79+
// Allow another window to schedule
80+
Interlocked.Exchange(ref _flushScheduled, 0);
81+
82+
// If more arrived after finished, schedule again
83+
if (!_pending.IsEmpty && Interlocked.Exchange(ref _flushScheduled, 1) == 0)
84+
{
85+
_ = FlushAfterDelayAsync(ct);
86+
}
87+
}
88+
}
89+
90+
public void Dispose()
91+
{
92+
_cts?.Cancel();
93+
_cts?.Dispose();
94+
_cts = null;
95+
_pending.Clear();
96+
}
97+
}

Zolian.Server.Base/Network/Client/Coalescer/SoundCoalescer.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ private async Task FlushAfterDelayAsync(CancellationToken ct)
7575
}
7676
}
7777

78-
// Hard cap protection: drop anything still pending this window
78+
// Hard cap protection: drop any remaining this window
7979
if (!_pending.IsEmpty)
8080
_pending.Clear();
8181
}

Zolian.Server.Base/Network/Client/WorldClient.cs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ public class WorldClient : WorldClientBase, IWorldClient
6262
private readonly IWorldServer<WorldClient> _server;
6363
private readonly SoundCoalescer _soundCoalescer;
6464
private readonly HealthBarCoalescer _healthBarCoalescer;
65+
private readonly BodyAnimationCoalescer _bodyAnimationCoalescer;
6566

6667
public readonly WorldServerTimer SkillSpellTimer = new(TimeSpan.FromMilliseconds(1000));
6768
public readonly Stopwatch CooldownControl = new();
@@ -172,6 +173,7 @@ public WorldClient([NotNull] IWorldServer<IWorldClient> server, [NotNull] Socket
172173
_server = server;
173174
_soundCoalescer = new SoundCoalescer(SendSoundImmediate, 150, 32);
174175
_healthBarCoalescer = new HealthBarCoalescer(SendHealthBarCoalesced, 150, 32);
176+
_bodyAnimationCoalescer = new BodyAnimationCoalescer(SendBodyAnimationCoalesced, 150, 32);
175177

176178
// Event-Driven Tasks
177179
Task.Factory.StartNew(ProcessExperienceEvents, TaskCreationOptions.LongRunning);
@@ -1720,6 +1722,11 @@ public void SendBoardResponse(BoardOrResponseType responseType, string message,
17201722
Send(args);
17211723
}
17221724

1725+
/// <summary>
1726+
/// 0x1A - Player Body Animation - Coalesced Send
1727+
/// </summary>
1728+
private void SendBodyAnimationCoalesced(BodyAnimationArgs args) => Send(args);
1729+
17231730
/// <summary>
17241731
/// 0x1A - Player Body Animation
17251732
/// </summary>
@@ -1735,7 +1742,7 @@ public void SendBodyAnimation(uint id, BodyAnimation bodyAnimation, ushort speed
17351742
AnimationSpeed = speed
17361743
};
17371744

1738-
Send(args);
1745+
_bodyAnimationCoalescer.Enqueue(args);
17391746
}
17401747

17411748
/// <summary>

0 commit comments

Comments
 (0)