Skip to content

Commit 05c0465

Browse files
committed
Ping Optimization (EMA) & Adv Stats
1 parent 48f7b63 commit 05c0465

File tree

5 files changed

+187
-93
lines changed

5 files changed

+187
-93
lines changed
Lines changed: 133 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
1-
using Darkages.Common;
1+
using System.Globalization;
2+
using System.Text;
3+
4+
using Darkages.Common;
25
using Darkages.Enums;
36
using Darkages.Network.Client;
47
using Darkages.Network.Server;
58
using Darkages.ScriptingBase;
69
using Darkages.Sprites.Entity;
7-
using System.Globalization;
8-
using System.Text.RegularExpressions;
910

1011
namespace Darkages.GameScripts.Mundanes.Generic;
1112

@@ -22,79 +23,117 @@ protected override void TopMenu(WorldClient client)
2223
{
2324
base.TopMenu(client);
2425

25-
var level = client.Aisling.ExpLevel;
26-
var ability = client.Aisling.AbpLevel;
27-
var bStr = client.Aisling._Str.ToString("D3");
28-
var baseStr = Regex.Replace(bStr, @"\b0+", m => "".PadLeft(m.Value.Length, ' '));
29-
var bInt = client.Aisling._Int.ToString("D3");
30-
var baseInt = Regex.Replace(bInt, @"\b0+", m => "".PadLeft(m.Value.Length, ' '));
31-
var bWis = client.Aisling._Wis.ToString("D3");
32-
var baseWis = Regex.Replace(bWis, @"\b0+", m => "".PadLeft(m.Value.Length, ' '));
33-
var bCon = client.Aisling._Con.ToString("D3");
34-
var baseCon = Regex.Replace(bCon, @"\b0+", m => "".PadLeft(m.Value.Length, ' '));
35-
var bDex = client.Aisling._Dex.ToString("D3");
36-
var baseDex = Regex.Replace(bDex, @"\b0+", m => "".PadLeft(m.Value.Length, ' '));
37-
var gStr = client.Aisling.BonusStr.ToString("D3");
38-
var gearStr = Regex.Replace(gStr, @"\b0+", m => "".PadLeft(m.Value.Length, ' '));
39-
var gInt = client.Aisling.BonusInt.ToString("D3");
40-
var gearInt = Regex.Replace(gInt, @"\b0+", m => "".PadLeft(m.Value.Length, ' '));
41-
var gWis = client.Aisling.BonusWis.ToString("D3");
42-
var gearWis = Regex.Replace(gWis, @"\b0+", m => "".PadLeft(m.Value.Length, ' '));
43-
var gCon = client.Aisling.BonusCon.ToString("D3");
44-
var gearCon = Regex.Replace(gCon, @"\b0+", m => "".PadLeft(m.Value.Length, ' '));
45-
var gDex = client.Aisling.BonusDex.ToString("D3");
46-
var gearDex = Regex.Replace(gDex, @"\b0+", m => "".PadLeft(m.Value.Length, ' '));
47-
var pStr = client.Aisling.Str.ToString("D3");
48-
var playerStr = Regex.Replace(pStr, @"\b0+", m => "".PadLeft(m.Value.Length, ' '));
49-
var pInt = client.Aisling.Int.ToString("D3");
50-
var playerInt = Regex.Replace(pInt, @"\b0+", m => "".PadLeft(m.Value.Length, ' '));
51-
var pWis = client.Aisling.Wis.ToString("D3");
52-
var playerWis = Regex.Replace(pWis, @"\b0+", m => "".PadLeft(m.Value.Length, ' '));
53-
var pCon = client.Aisling.Con.ToString("D3");
54-
var playerCon = Regex.Replace(pCon, @"\b0+", m => "".PadLeft(m.Value.Length, ' '));
55-
var pDex = client.Aisling.Dex.ToString("D3");
56-
var playerDex = Regex.Replace(pDex, @"\b0+", m => "".PadLeft(m.Value.Length, ' '));
57-
var playerDmg = client.Aisling.Dmg.ToString();
58-
var playerAc = client.Aisling.SealedAc.ToString();
59-
var playerFort = client.Aisling.Fortitude.ToString(CultureInfo.CurrentCulture);
60-
var playerReflex = client.Aisling.Reflex.ToString(CultureInfo.CurrentCulture);
61-
var playerWill = client.Aisling.Will.ToString(CultureInfo.CurrentCulture);
62-
var playerRegen = client.Aisling.Regen.ToString();
63-
var playerBleeding = client.Aisling.Bleeding.ToString();
64-
var playerRending = client.Aisling.Rending.ToString();
65-
var playerAegis = client.Aisling.Aegis.ToString();
66-
var playerReaping = client.Aisling.Reaping.ToString();
67-
var playerVamp = client.Aisling.Vampirism.ToString();
68-
var playerGhost = client.Aisling.Ghosting.ToString();
69-
var playerHaste = client.Aisling.Haste.ToString();
70-
var playerSpikes = client.Aisling.Spikes.ToString();
71-
var playerGust = client.Aisling.Gust.ToString();
72-
var playerQuake = client.Aisling.Quake.ToString();
73-
var playerRain = client.Aisling.Rain.ToString();
74-
var playerFlame = client.Aisling.Flame.ToString();
75-
var playerDusk = client.Aisling.Dusk.ToString();
76-
var playerDawn = client.Aisling.Dawn.ToString();
77-
var playerOffElement = ElementManager.ElementValue(client.Aisling.SecondaryOffensiveElement);
78-
var playerDefElement = ElementManager.ElementValue(client.Aisling.SecondaryDefensiveElement);
79-
var amplified = (client.Aisling.Amplified * 100).ToString(CultureInfo.CurrentCulture);
80-
var latency = client.Latency.Elapsed;
81-
var latencyMs = $"{client.Latency.Elapsed.Milliseconds} ms";
82-
var latencyCode = ColorCodeLatency(latency);
83-
var mapNum = client.Aisling.Map.ID;
84-
var playerBoxed = client.Aisling.ExpTotal;
26+
var a = client.Aisling;
27+
var level = a.ExpLevel;
28+
var ability = a.AbpLevel;
29+
var baseStr = D3(a._Str);
30+
var baseInt = D3(a._Int);
31+
var baseWis = D3(a._Wis);
32+
var baseCon = D3(a._Con);
33+
var baseDex = D3(a._Dex);
34+
var gearStr = D3(a.BonusStr);
35+
var gearInt = D3(a.BonusInt);
36+
var gearWis = D3(a.BonusWis);
37+
var gearCon = D3(a.BonusCon);
38+
var gearDex = D3(a.BonusDex);
39+
var playerStr = D3(a.Str);
40+
var playerInt = D3(a.Int);
41+
var playerWis = D3(a.Wis);
42+
var playerCon = D3(a.Con);
43+
var playerDex = D3(a.Dex);
44+
45+
var playerDmg = a.Dmg;
46+
var playerAc = a.SealedAc;
47+
var playerRegen = a.Regen;
48+
49+
var fort = a.Fortitude.ToString("0.#", CultureInfo.InvariantCulture);
50+
var reflex = a.Reflex.ToString("0.#", CultureInfo.InvariantCulture);
51+
var will = a.Will.ToString("0.#", CultureInfo.InvariantCulture);
52+
var playerOffPriElement = ElementManager.ElementValue(a.OffenseElement);
53+
var playerDefPriElement = ElementManager.ElementValue(a.DefenseElement);
54+
var playerOffSecElement = ElementManager.ElementValue(a.SecondaryOffensiveElement);
55+
var playerDefSecElement = ElementManager.ElementValue(a.SecondaryDefensiveElement);
56+
var amplifiedPct = (a.Amplified * 100).ToString("0.#", CultureInfo.InvariantCulture);
57+
58+
var rtt = client.LastRttMs;
59+
var avg = client.SmoothedRttMs;
60+
var rttCode = ColorCodeLatency(rtt);
61+
var avgCode = ColorCodeLatency(avg);
62+
63+
// Non-color cue for spikes (colorblind helper)
64+
static string Cue(int ms) => ms >= 250 ? "!!" : ms >= 150 ? "!" : "";
65+
66+
var mapNum = a.Map.ID;
8567
if (mapNum is >= 800 and <= 810) mapNum = 0;
8668

87-
client.SendServerMessage(ServerMessageType.ScrollWindow, $"{{=gMap#: {{=a{mapNum} {{=gInsight: {{=b{level} {{=gRank: {{=b{ability} {{=gLatency: {{={latencyCode}{latencyMs}\n" +
88-
$"{{=gBase Stats| {{=cS:{{=a{baseStr}{{=c, I:{{=a{baseInt}{{=c, W:{{=a{baseWis}{{=c, C:{{=a{baseCon}{{=c, D:{{=a{baseDex}\n" +
89-
$"{{=gGear Stats| {{=cS:{{=a{gearStr}{{=c, I:{{=a{gearInt}{{=c, W:{{=a{gearWis}{{=c, C:{{=a{gearCon}{{=c, D:{{=a{gearDex}\n" +
90-
$"{{=gFull Stats| {{=cS:{{=a{playerStr}{{=c, I:{{=a{playerInt}{{=c, W:{{=a{playerWis}{{=c, C:{{=a{playerCon}{{=c, D:{{=a{playerDex}\n" +
91-
$" {{=gOffense| {{=cDMG{{=c:{{=a{playerDmg}{{=c, {{=sAmp{{=c:{{=a{amplified}%{{=c, {{=sSecondary{{=c:{{=a{playerOffElement}\n" +
92-
$" {{=gDefense| {{=sAC{{=c:{{=a{playerAc}{{=c, {{=gRegen{{=c:{{=a{playerRegen}{{=c, {{=sSecondary{{=c:{{=a{playerDefElement}\n" +
93-
$"{{=cExp Boxed: {{=a{playerBoxed}\n\n" +
94-
$"{{=eSaving Throws{{=c: {{=sFort{{=c:{{=a{playerFort}%{{=c, {{=sReflex{{=c:{{=a{playerReflex}%{{=c, {{=sWill{{=c:{{=a{playerWill}%\n" +
95-
$"{{=bBleeding{{=c: {{=a{playerBleeding}{{=c, {{=rRending{{=c: {{=a{playerRending}{{=c, {{=sAegis{{=c: {{=a{playerAegis}{{=c, {{=nReaping{{=c: {{=a{playerReaping}\n" +
96-
$"{{=bVamp{{=c: {{=a{playerVamp}{{=c, {{=uGhost{{=c: {{=a{playerGhost}{{=c, {{=cHaste{{=c: {{=a{playerHaste}{{=c, {{=wSpikes{{=c: {{=a{playerSpikes}, {{=uGust{{=c: {{=a{playerGust}{{=c\n" +
97-
$"{{=uQuake{{=c: {{=a{playerQuake}{{=c, {{=uRain{{=c: {{=a{playerRain}, {{=uFlame{{=c: {{=a{playerFlame}{{=c, {{=uDusk{{=c: {{=a{playerDusk}{{=c, {{=uDawn{{=c: {{=a{playerDawn}");
69+
var sb = new StringBuilder(900);
70+
71+
// Player Info Card
72+
sb.Append("{=q").Append(a.Username)
73+
.Append(" {=gLvl: {=b").Append(level)
74+
.Append(" {=gJob: {=b").Append(ability)
75+
.Append(" {=gIns: {=b").Append(level + ability)
76+
.Append('\n');
77+
78+
// Map, Ping, EMA Latency
79+
sb.Append("{=gMap#: {=e").Append(mapNum)
80+
.Append(" {=gGrid: {=e").Append(a.Position.X).Append("{=c,").Append("{=e").Append(a.Position.Y)
81+
.Append(" {=gPing: {=").Append(rttCode).Append(rtt).Append("{=ams").Append(Cue(rtt))
82+
.Append(" {=gAvg: {=").Append(avgCode).Append(avg).Append("{=ams").Append(Cue(avg))
83+
.Append('\n');
84+
85+
// Stats Blocks
86+
sb.Append("{=gBase| {=cS:{=a").Append(baseStr).Append("{=c, I:{=a").Append(baseInt)
87+
.Append("{=c, W:{=a").Append(baseWis).Append("{=c, C:{=a").Append(baseCon)
88+
.Append("{=c, D:{=a").Append(baseDex).Append('\n');
89+
90+
sb.Append("{=gGear| {=cS:{=a").Append(gearStr).Append("{=c, I:{=a").Append(gearInt)
91+
.Append("{=c, W:{=a").Append(gearWis).Append("{=c, C:{=a").Append(gearCon)
92+
.Append("{=c, D:{=a").Append(gearDex).Append('\n');
93+
94+
sb.Append("{=gMax | {=cS:{=a").Append(playerStr).Append("{=c, I:{=a").Append(playerInt)
95+
.Append("{=c, W:{=a").Append(playerWis).Append("{=c, C:{=a").Append(playerCon)
96+
.Append("{=c, D:{=a").Append(playerDex).Append('\n');
97+
98+
// Offense / Defense
99+
sb.Append("{=gOff | {=sDMG{=c:{=a").Append(playerDmg).Append("{=c, {=sAmp{=c:{=a").Append(amplifiedPct)
100+
.Append("{=c%, {=sPri{=c:{=a").Append(playerOffPriElement).Append("{=c, {=sSec{=c:{=a").Append(playerOffSecElement)
101+
.Append('\n');
102+
103+
sb.Append("{=gDef | {=sAC{=c:{=a").Append(playerAc).Append("{=c, {=sRegen{=c:{=a").Append(playerRegen)
104+
.Append("{=c%, {=sPri{=c:{=a").Append(playerDefPriElement).Append("{=c, {=sSec{=c:{=a").Append(playerDefSecElement)
105+
.Append('\n');
106+
107+
// Saving Throws
108+
sb.Append("{=eSAVE{=g| {=sFort{=c:{=a").Append(fort).Append("{=c%, ")
109+
.Append("{=sRef{=c:{=a").Append(reflex).Append("{=c%, ")
110+
.Append("{=sWill{=c:{=a").Append(will).Append("{=c%")
111+
.Append("\n\n");
112+
113+
// Exp
114+
sb.Append("{=cExp Boxed: {=a").Append(a.ExpTotal).Append(" {=q--Buffs Below").Append('\n');
115+
116+
// Buff Grid
117+
sb.Append("{=bBleed{=c: {=").Append(BuffColor(a.Bleeding)).Append(a.Bleeding)
118+
.Append("{=c, {=rRend{=c: {=").Append(BuffColor(a.Rending)).Append(a.Rending)
119+
.Append("{=c, {=sAegis{=c: {=").Append(BuffColor(a.Aegis)).Append(a.Aegis)
120+
.Append("{=c, {=nReap{=c: {=").Append(BuffColor(a.Reaping)).Append(a.Reaping)
121+
.Append('\n');
122+
123+
sb.Append("{=bVamp{=c: {=").Append(BuffColor(a.Vampirism)).Append(a.Vampirism)
124+
.Append("{=c, {=uGhost{=c: {=").Append(BuffColor(a.Ghosting)).Append(a.Ghosting)
125+
.Append("{=c, {=cHaste{=c: {=").Append(BuffColor(a.Haste)).Append(a.Haste)
126+
.Append("{=c, {=wSpikes{=c: {=").Append(BuffColor(a.Spikes)).Append(a.Spikes)
127+
.Append("{=c, {=uGust{=c: {=").Append(BuffColor(a.Gust)).Append(a.Gust)
128+
.Append('\n');
129+
130+
sb.Append("{=uQuake{=c: {=").Append(BuffColor(a.Quake)).Append(a.Quake)
131+
.Append("{=c, {=uRain{=c: {=").Append(BuffColor(a.Rain)).Append(a.Rain)
132+
.Append("{=c, {=uFlame{=c: {=").Append(BuffColor(a.Flame)).Append(a.Flame)
133+
.Append("{=c, {=uDusk{=c: {=").Append(BuffColor(a.Dusk)).Append(a.Dusk)
134+
.Append("{=c, {=uDawn{=c: {=").Append(BuffColor(a.Dawn)).Append(a.Dawn);
135+
136+
client.SendServerMessage(ServerMessageType.ScrollWindow, sb.ToString());
98137
}
99138

100139
public override void OnResponse(WorldClient client, ushort responseID, string args)
@@ -105,10 +144,26 @@ public override void OnResponse(WorldClient client, ushort responseID, string ar
105144
}
106145
}
107146

108-
private static string ColorCodeLatency(TimeSpan time)
147+
private static string ColorCodeLatency(int time)
109148
{
110-
if (time < TimeSpan.FromMilliseconds(100)) return "q";
111-
if (time < TimeSpan.FromMilliseconds(200)) return "c";
112-
return time >= TimeSpan.FromMilliseconds(250) ? "b" : "n";
149+
if (time < 100) return "q";
150+
if (time < 200) return "c";
151+
return time >= 250 ? "n" : "b";
152+
}
153+
154+
private static string BuffColor(int value) => value > 0 ? "q" : "a";
155+
156+
private static string D3(int value)
157+
{
158+
if (value < 0) value = 0;
159+
if (value > 1000) value = 1000;
160+
161+
return value switch
162+
{
163+
< 10 => $" {value}",
164+
< 100 => $" {value}",
165+
< 1000 => $" {value}",
166+
_ => value.ToString(CultureInfo.InvariantCulture)
167+
};
113168
}
114169
}

Zolian.Server.Base/Network/Client/Abstractions/IWorldClient.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,6 @@ public interface IWorldClient : IConnectedClient
3030
DateTime LastMessageSent { get; set; }
3131
DateTime LastMovement { get; set; }
3232
DateTime LastEquip { get; set; }
33-
Stopwatch Latency { get; set; }
3433
DateTime LastWhisperMessageSent { get; set; }
3534
uint EntryCheck { get; set; }
3635
void LoadSkillBook();

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

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,16 @@ public class WorldClient : WorldClientBase, IWorldClient
6262
public Spell LastSpell = new();
6363
public bool ExitConfirmed;
6464

65+
// Ping/Pong RTT
66+
public int HeartBeatInFlight;
67+
public long HeartBeatStartTimestamp;
68+
public int HeartBeatIdx;
69+
public int HeartBeatCount;
70+
public readonly int[] HeartBeatSamplesMs = new int[3];
71+
public int LastRttMs { get; set; }
72+
public int RollingRtt15sMs { get; set; }
73+
public int SmoothedRttMs { get; set; }
74+
6575
private readonly Dictionary<string, Stopwatch> _clientStopwatches = new()
6676
{
6777
{ "Affliction", new Stopwatch() },
@@ -132,7 +142,6 @@ public bool IsDayDreaming
132142
public DateTime LastMessageSent { get; set; }
133143
public DateTime LastMovement { get; set; }
134144
public DateTime LastEquip { get; set; }
135-
public Stopwatch Latency { get; set; } = new();
136145
public DateTime LastSave { get; set; }
137146
public DateTime LastWhisperMessageSent { get; set; }
138147
public PendingBuy PendingBuySessions { get; set; }
@@ -2753,13 +2762,15 @@ private void ObtainProfileLegendMarks(object sender, NotifyCollectionChangedEven
27532762

27542763
public override void SendHeartBeat(byte first, byte second)
27552764
{
2765+
if (Interlocked.CompareExchange(ref HeartBeatInFlight, 1, 0) != 0) return;
2766+
Volatile.Write(ref HeartBeatStartTimestamp, Stopwatch.GetTimestamp());
2767+
27562768
var args = new HeartBeatResponseArgs
27572769
{
27582770
First = first,
27592771
Second = second
27602772
};
27612773

2762-
Latency.Restart();
27632774
Send(args);
27642775
}
27652776

Zolian.Server.Base/Network/Components/PingComponent.cs

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@ namespace Darkages.Network.Components;
66

77
public class PingComponent(WorldServer server) : WorldServerComponent(server)
88
{
9-
private const int ComponentSpeed = 7000;
9+
private const int ComponentSpeed = 5000;
10+
private const byte HeartbeatFirst = 0x14;
11+
private const byte HeartbeatSecond = 0x20;
1012

1113
protected internal override async Task Update()
1214
{
@@ -22,7 +24,7 @@ protected internal override async Task Update()
2224

2325
// Clamp to avoid super tiny delays
2426
if (remaining > 0)
25-
await Task.Delay(Math.Min(remaining, 50));
27+
await Task.Delay(Math.Min(remaining, 10));
2628
continue;
2729
}
2830

@@ -43,17 +45,11 @@ private static void Ping()
4345
{
4446
foreach (var player in Server.Aislings)
4547
{
46-
if (player?.Client == null) return;
47-
if (!player.LoggedIn) return;
48-
4948
try
5049
{
51-
player.Client.SendHeartBeat(0x20, 0x14);
52-
}
53-
catch (Exception ex)
54-
{
55-
SentrySdk.CaptureException(ex);
50+
player?.Client?.SendHeartBeat(HeartbeatFirst, HeartbeatSecond);
5651
}
52+
catch { }
5753
}
5854
}
5955
}

Zolian.Server.Base/Network/Server/WorldServer.cs

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,14 @@
55
using System.Net.Sockets;
66
using System.Numerics;
77
using System.Text;
8+
89
using Chaos.Cryptography;
910
using Chaos.Networking.Abstractions;
1011
using Chaos.Networking.Abstractions.Definitions;
1112
using Chaos.Networking.Entities.Client;
1213
using Chaos.Packets;
1314
using Chaos.Packets.Abstractions;
15+
1416
using Darkages.CommandSystem;
1517
using Darkages.Common;
1618
using Darkages.Database;
@@ -28,9 +30,13 @@
2830
using Darkages.Sprites.Entity;
2931
using Darkages.Templates;
3032
using Darkages.Types;
33+
3134
using JetBrains.Annotations;
35+
3236
using Microsoft.Extensions.Logging;
37+
3338
using ServiceStack;
39+
3440
using IWorldClient = Darkages.Network.Client.Abstractions.IWorldClient;
3541
using MapFlags = Darkages.Enums.MapFlags;
3642
using Redirect = Chaos.Networking.Entities.Redirect;
@@ -2685,8 +2691,35 @@ public ValueTask OnHeartBeatAsync(IWorldClient client, in Packet clientPacket)
26852691

26862692
static ValueTask InnerOnHeartBeat(IWorldClient localClient, HeartBeatArgs localArgs)
26872693
{
2688-
if (localArgs.First != 20 || localArgs.Second != 32) return default;
2689-
localClient.Latency.Stop();
2694+
if (localArgs.First != 0x20 || localArgs.Second != 0x14) return default;
2695+
var client = localClient.Aisling.Client;
2696+
if (Interlocked.Exchange(ref client.HeartBeatInFlight, 0) == 0) return default;
2697+
2698+
var start = Volatile.Read(ref client.HeartBeatStartTimestamp);
2699+
if (start <= 0) return default;
2700+
2701+
var now = Stopwatch.GetTimestamp();
2702+
var rttMs = (int)Math.Round((now - start) * 1000.0 / Stopwatch.Frequency);
2703+
if (rttMs < 0) rttMs = 0;
2704+
2705+
client.LastRttMs = rttMs;
2706+
2707+
// Rolling mean calculation
2708+
client.HeartBeatSamplesMs[client.HeartBeatIdx] = rttMs;
2709+
client.HeartBeatIdx = (client.HeartBeatIdx + 1) % client.HeartBeatSamplesMs.Length;
2710+
if (client.HeartBeatCount < client.HeartBeatSamplesMs.Length) client.HeartBeatCount++;
2711+
2712+
int sum = 0;
2713+
for (int i = 0; i < client.HeartBeatCount; i++)
2714+
sum += client.HeartBeatSamplesMs[i];
2715+
2716+
client.RollingRtt15sMs = (int)Math.Round(sum / (double)client.HeartBeatCount);
2717+
2718+
// EMA Smoothing
2719+
const double alpha = 0.25;
2720+
client.SmoothedRttMs = client.SmoothedRttMs == 0
2721+
? rttMs
2722+
: (int)Math.Round(client.SmoothedRttMs + alpha * (rttMs - client.SmoothedRttMs));
26902723

26912724
return default;
26922725
}

0 commit comments

Comments
 (0)