Skip to content

Commit 8d2d96b

Browse files
Initial implementation of two way ping
1 parent 2be686e commit 8d2d96b

8 files changed

Lines changed: 136 additions & 37 deletions

File tree

fCraft/Commands/InfoCommands.cs

Lines changed: 19 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1781,8 +1781,10 @@ private static void ExtraInfoHandler(Player player, CommandReader cmd) {
17811781
player.Message(" (Use &H/TPP X Y Z R L&S or &H/TPP {0}&S)", info.Name);
17821782
}
17831783
}
1784-
if (target != null && target.PingList.Any())
1785-
player.Message(" Ping: {0}ms Avg: {1}ms", target.PingList[9], target.PingList.Average());
1784+
1785+
if (target != null && target.AveragePingMilliseconds() != 0) {
1786+
player.Message(target.FormatPing());
1787+
}
17861788
}
17871789

17881790
#endregion
@@ -2093,24 +2095,26 @@ static void PluginsHandler(Player player, CommandReader cmd) {
20932095

20942096
static void PingListHandler(Player player, CommandReader cmd) {
20952097
string offsetStr = cmd.Next();
2096-
string value;
20972098
int offset = 0;
2098-
if (!int.TryParse(offsetStr, out offset)) {
2099-
offset = 0;
2100-
}
2101-
Player[] visiblePlayers = Server.Players.Where(p => p.PingList.Average() != 0 && player.CanSee(p)).OrderBy(p => p.PingList.Average()).Reverse().ToArray();
2102-
if (visiblePlayers.Count() < 1) {
2103-
player.Message("No players online right now");
2099+
if (!int.TryParse(offsetStr, out offset)) offset = 0;
2100+
2101+
Player[] candidates = Server.Players.CanBeSeen(player)
2102+
.Where(p => p.AveragePingMilliseconds() != 0)
2103+
.OrderBy(p => p.AveragePingMilliseconds()).Reverse().ToArray();
2104+
if (candidates.Length < 1) {
2105+
player.Message("No online players have clients supporting measuring ping.");
21042106
return;
21052107
}
2106-
Player[] playerList = visiblePlayers.Skip(fixOffset(offset, visiblePlayers.Count())).Take(10).ToArray();
2107-
int pad = string.Format("Ping: {0}ms Avg: {1:N0}ms", playerList[0].PingList[9], playerList[0].PingList.Average()).Length;
2108+
2109+
Player[] list = candidates.Skip(fixOffset(offset, candidates.Count())).Take(10).ToArray();
2110+
int pad = list[0].FormatPing().Length;
21082111
player.Message("Ping/Latency List:");
2109-
for (int i = 0; i < playerList.Count(); i++) {
2110-
value = string.Format("Ping: {0}ms Avg: {1:N0}ms", playerList[i].PingList[9], playerList[i].PingList.Average());
2111-
player.Message(" &7{1}&S - {0}", playerList[i].Info.ClassyName, value.PadLeft(pad, '0'));
2112+
2113+
for (int i = 0; i < list.Length; i++) {
2114+
player.Message(" &7{1}&S - {0}", list[i].Info.ClassyName,
2115+
list[i].FormatPing().PadLeft(pad, '0'));
21122116
}
2113-
player.Message("Showing players {0}-{1} (out of {2}).", offset + 1, offset + playerList.Length, visiblePlayers.Count());
2117+
player.Message("Showing players {0}-{1} (out of {2}).", offset + 1, offset + list.Length, candidates.Count());
21142118
}
21152119

21162120
static int fixOffset(int origOffset, int allPlayerCount) {

fCraft/Network/CpeConstants.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,9 @@ public enum CpeExt
9898

9999
/// <summary> Allows for setting entity properties, such as rotation on X/Z axis. </summary>
100100
EntityProperty,
101+
102+
/// <summary> Allows both client and server to measure average ping. </summary>
103+
TwoWayPing,
101104
}
102105

103106

fCraft/Network/OpCode.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,9 @@ public enum OpCode
136136

137137
/// <summary> Packet telling the client to update an aspect of the given entity. </summary>
138138
SetEntityProperty = 42,
139+
140+
/// <summary> This extension allows both client and server to measure average ping. </summary>
141+
TwoWayPing = 43,
139142
}
140143
}
141144

fCraft/Network/Packet.CPE.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -350,5 +350,14 @@ public static Packet MakeEntityProperty( sbyte id, EntityProp prop, int value )
350350
WriteI32( value, packet.Bytes, 3 );
351351
return packet;
352352
}
353+
354+
355+
[Pure]
356+
public static Packet MakeTwoWayPing( bool serverToClient, ushort data ) {
357+
Packet packet = new Packet( OpCode.TwoWayPing );
358+
packet.Bytes[1] = (byte)(serverToClient ? 1 : 0);
359+
WriteU16( data, packet.Bytes, 2 );
360+
return packet;
361+
}
353362
}
354363
}

fCraft/Network/Packet.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -305,6 +305,7 @@ internal static void WriteI32( int number, [NotNull] byte[] arr, int offset ) {
305305
65, // SetEnvMapUrl
306306
6, // SetEnvMapProperty
307307
7, // SetEntityProperty
308+
4, // TwoWayPing
308309
};
309310
}
310311

fCraft/Network/Player.Handshake.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -367,6 +367,9 @@ bool NegotiateProtocolExtension() {
367367
case EntityPropertyExtName:
368368
if (version == 1) ext = CpeExt.EntityProperty;
369369
break;
370+
case TwoWayPingExtName:
371+
if (version == 1) ext = CpeExt.TwoWayPing;
372+
break;
370373
}
371374
if (ext != CpeExt.None)
372375
supportedExts.Add(ext);

fCraft/Network/Player.Networking.cs

Lines changed: 54 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@ public sealed partial class Player {
2424
const int SocketPollInterval = 100; // multiples of SleepDelay, approx. 1 second
2525
const int PingInterval = 1; // multiples of SocketPollInterval, approx. 3 seconds
2626
public DateTime LastZoneNotification = DateTime.UtcNow;
27-
public Stopwatch pingTimer = new Stopwatch();
2827

2928
static Player() {
3029
MaxBlockPlacementRange = 32767;
@@ -156,11 +155,15 @@ void IoLoop() {
156155
LeaveReason = LeaveReason.ClientQuit;
157156
return;
158157
}
158+
159159
if( pingCounter > PingInterval ) {
160-
writer.Write( OpCode.Ping );
161-
pingTimer.Reset();
162-
pingTimer.Start();
163-
BytesSent++;
160+
if( Supports( CpeExt.TwoWayPing ) ) {
161+
SendNow( Packet.MakeTwoWayPing( true, NextTwoWayPingData() ) );
162+
} else {
163+
writer.Write( OpCode.Ping );
164+
BytesSent++;
165+
}
166+
164167
pingCounter = 0;
165168
MeasureBandwidthUseRates();
166169
}
@@ -273,6 +276,10 @@ void IoLoop() {
273276
case OpCode.PlayerClick:
274277
ProcessPlayerClickPacket();
275278
break;
279+
280+
case OpCode.TwoWayPing:
281+
ProcessTwoWayPingPacket();
282+
break;
276283

277284
case OpCode.Ping:
278285
ProcessPingPacket();
@@ -313,21 +320,29 @@ void IoLoop() {
313320
}
314321
}
315322
#endregion
323+
324+
ushort NextTwoWayPingData() {
325+
// Find free ping slot
326+
for( int i = 0; i < PingList.Length; i++ ) {
327+
if( PingList[i].TimeSent.Ticks == 0 ) continue;
328+
329+
ushort prev = i > 0 ? PingList[i - 1].Data : (ushort)0;
330+
PingList[i].Data = (ushort)(prev + 1);
331+
PingList[i].TimeSent = DateTime.UtcNow;
332+
return (ushort)(prev + 1);
333+
}
334+
335+
// Remove oldest ping slot
336+
for( int i = 0; i < PingList.Length - 1; i++ ) {
337+
PingList[i] = PingList[i + 1];
338+
}
339+
340+
PingList[PingList.Length - 1].Data++;
341+
PingList[PingList.Length - 1].TimeSent = DateTime.UtcNow;
342+
return PingList[PingList.Length - 1].Data;
343+
}
316344

317-
public void ProcessPingPacket() {
318-
pingTimer.Stop();
319-
int total = 0;
320-
for( int i = 0; i < 10; i++) {
321-
if (i == 9) {
322-
PingList[i] = (int)pingTimer.ElapsedMilliseconds;
323-
} else {
324-
PingList[i] = PingList[i + 1];
325-
}
326-
total += PingList[i];
327-
}
328-
if (!IsPlayingCTF) {
329-
Message((byte)MessageType.BottomRight3, "&SPing: &f{0}&Sms Avg: &f{1}&Sms", PingList[9], PingList.Average());
330-
}
345+
void ProcessPingPacket() {
331346
BytesReceived++;
332347
}
333348

@@ -518,7 +533,7 @@ void ProcessSetBlockPacket() {
518533
}
519534

520535
PlaceBlockWithEvents( coords, action, (Block)type );
521-
}
536+
}
522537

523538
void ProcessPlayerClickPacket() {
524539
BytesReceived += 15;
@@ -532,6 +547,25 @@ void ProcessPlayerClickPacket() {
532547
short targetBlockZ = reader.ReadInt16();
533548
byte targetBlockFace = reader.ReadByte();
534549
}
550+
551+
void ProcessTwoWayPingPacket() {
552+
bool serverToClient = reader.ReadByte() != 0;
553+
ushort data = reader.ReadUInt16();
554+
BytesReceived += 3;
555+
556+
// Client-> server ping, immediately reply.
557+
if( !serverToClient ) {
558+
SendNow( Packet.MakeTwoWayPing( false, data ) );
559+
return;
560+
}
561+
562+
// Got a response for a server->client ping, set the time received.
563+
for( int i = 0; i < PingList.Length; i++ ) {
564+
if( PingList[i].Data != data ) continue;
565+
PingList[i].TimeReceived = DateTime.UtcNow;
566+
break;
567+
}
568+
}
535569

536570

537571
void Disconnect() {

fCraft/Player/Player.cs

Lines changed: 44 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,12 @@ public bool IsStaff {
164164
internal int currentBDStep = -1;
165165
internal BlockDefinition currentBD;
166166

167-
public int[] PingList = new int[10];
167+
public struct Ping {
168+
public DateTime TimeSent, TimeReceived;
169+
public ushort Data;
170+
}
171+
public Ping[] PingList = new Ping[20];
172+
168173
public string LastMotdMessage;
169174
public Player LastPlayerGreeted = Player.Console;
170175

@@ -2142,6 +2147,7 @@ public void ResetIdleTimer()
21422147
const string EnvMapAspectExtName = "EnvMapAspect";
21432148
const string ExtPlayerPositionsExtName = "ExtEntityPositions";
21442149
const string EntityPropertyExtName = "EntityProperty";
2150+
const string TwoWayPingExtName = "TwoWayPing";
21452151

21462152
bool supportsBlockDefs, supportsCustomBlocks;
21472153
internal bool supportsExtPositions;
@@ -2250,7 +2256,43 @@ public void Kick( [NotNull] Player player, [CanBeNull] string reason, LeaveReaso
22502256
}
22512257

22522258
#endregion
2253-
2259+
2260+
2261+
2262+
/// <summary> Gets average ping in milliseconds, or 0 if no ping measures. </summary>
2263+
public double AveragePingMilliseconds() {
2264+
double totalMs = 0;
2265+
int measures = 0;
2266+
2267+
foreach (Ping ping in PingList) {
2268+
if (ping.TimeSent.Ticks == 0 || ping.TimeReceived.Ticks == 0) continue;
2269+
2270+
totalMs += (ping.TimeReceived - ping.TimeSent).TotalMilliseconds;
2271+
measures++;
2272+
}
2273+
return measures == 0 ? 0 : (totalMs / measures);
2274+
}
2275+
2276+
2277+
/// <summary> Gets worst ping in milliseconds, or 0 if no ping measures. </summary>
2278+
public double WorstPingMilliseconds() {
2279+
double totalMs = 0;
2280+
2281+
foreach (Ping ping in PingList) {
2282+
if (ping.TimeSent.Ticks == 0 || ping.TimeReceived.Ticks == 0) continue;
2283+
2284+
double ms = (ping.TimeReceived - ping.TimeSent).TotalMilliseconds;
2285+
totalMs = Math.Max(totalMs, ms);
2286+
}
2287+
return totalMs;
2288+
}
2289+
2290+
public string FormatPing() {
2291+
return String.Format(" Worst ping {0}ms, average {1}ms",
2292+
WorstPingMilliseconds().ToString("N0"),
2293+
AveragePingMilliseconds().ToString("N0"));
2294+
}
2295+
22542296

22552297
[CanBeNull]
22562298
public string LastUsedPlayerName { get; set; }

0 commit comments

Comments
 (0)