diff --git a/MCGalaxy/Commands/CPE/CmdPing.cs b/MCGalaxy/Commands/CPE/CmdPing.cs
new file mode 100644
index 000000000..c7765c8a4
--- /dev/null
+++ b/MCGalaxy/Commands/CPE/CmdPing.cs
@@ -0,0 +1,58 @@
+/*
+ Copyright 2015 MCGalaxy
+
+ Dual-licensed under the Educational Community License, Version 2.0 and
+ the GNU General Public License, Version 3 (the "Licenses"); you may
+ not use this file except in compliance with the Licenses. You may
+ obtain a copy of the Licenses at
+
+ http://www.opensource.org/licenses/ecl2.php
+ http://www.gnu.org/licenses/gpl-3.0.html
+
+ Unless required by applicable law or agreed to in writing,
+ software distributed under the Licenses are distributed on an "AS IS"
+ BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ or implied. See the Licenses for the specific language governing
+ permissions and limitations under the Licenses.
+ */
+using MCGalaxy.Network;
+
+namespace MCGalaxy.Commands.Chatting {
+ public sealed class CmdPing : Command {
+ public override string name { get { return "ping"; } }
+ public override string type { get { return CommandTypes.Information; } }
+ public override bool museumUsable { get { return true; } }
+ public override LevelPermission defaultRank { get { return LevelPermission.Guest; } }
+ public override CommandPerm[] ExtraPerms {
+ get { return new[] { new CommandPerm(LevelPermission.Admin, "+ can see ping of all players") }; }
+ }
+
+ public override void Use(Player p, string message) {
+ if (!message.CaselessEq("all")) {
+ if (Player.IsSuper(p)) { Player.Message(p, "Super users cannot measure their own ping."); return; }
+
+ if (p.Ping.AveragePingMilliseconds() == 0) {
+ Player.Message(p, "Your client does not support measuring ping.");
+ } else {
+ Player.Message(p, p.Ping.Format());
+ }
+ } else {
+ if (!CheckExtraPerm(p)) { MessageNeedExtra(p); return; }
+ Player[] players = PlayerInfo.Online.Items;
+ Player.Message(p, "Ping/latency list for online players:");
+
+ foreach (Player pl in players) {
+ if (!Entities.CanSee(p, pl)) continue;
+ if (pl.Ping.AveragePingMilliseconds() == 0) continue;
+ Player.Message(p, pl.ColoredName + " %S- " + pl.Ping.Format());
+ }
+ }
+ }
+
+ public override void Help(Player p) {
+ Player.Message(p, "%T/ping %H- Outputs details about your ping to the server.");
+ Player.Message(p, "%T/ping all %H- Outputs ping details for all players.");
+ Player.Message(p, "&cNOTE: %HNot all clients support measuring ping.");
+ }
+ }
+}
diff --git a/MCGalaxy/MCGalaxy_.csproj b/MCGalaxy/MCGalaxy_.csproj
index b2ec9a2a8..c052e430f 100644
--- a/MCGalaxy/MCGalaxy_.csproj
+++ b/MCGalaxy/MCGalaxy_.csproj
@@ -199,6 +199,7 @@
+
@@ -576,6 +577,7 @@
+
diff --git a/MCGalaxy/Network/Packets/Opcode.cs b/MCGalaxy/Network/Packets/Opcode.cs
index 35cbc0311..00ec7c7c3 100644
--- a/MCGalaxy/Network/Packets/Opcode.cs
+++ b/MCGalaxy/Network/Packets/Opcode.cs
@@ -66,5 +66,6 @@ namespace MCGalaxy.Network {
public const byte CpeSetMapEnvUrl = 40;
public const byte CpeSetMapEnvProperty = 41;
public const byte CpeSetEntityProperty = 42;
+ public const byte CpeTwoWayPing = 43;
}
}
diff --git a/MCGalaxy/Network/Packets/Packet.CPE.cs b/MCGalaxy/Network/Packets/Packet.CPE.cs
index 146ae02bd..2fbd8ac48 100644
--- a/MCGalaxy/Network/Packets/Packet.CPE.cs
+++ b/MCGalaxy/Network/Packets/Packet.CPE.cs
@@ -247,5 +247,13 @@ namespace MCGalaxy.Network {
NetUtils.WriteI32(value, buffer, 3);
return buffer;
}
+
+ public static byte[] TwoWayPing(bool serverToClient, ushort data) {
+ byte[] buffer = new byte[4];
+ buffer[0] = Opcode.CpeTwoWayPing;
+ buffer[1] = (byte)(serverToClient ? 1 : 0);
+ NetUtils.WriteU16(data, buffer, 2);
+ return buffer;
+ }
}
}
diff --git a/MCGalaxy/Network/Player.Networking.cs b/MCGalaxy/Network/Player.Networking.cs
index 704efef71..5ee8eb703 100644
--- a/MCGalaxy/Network/Player.Networking.cs
+++ b/MCGalaxy/Network/Player.Networking.cs
@@ -58,6 +58,19 @@ namespace MCGalaxy {
if (OnPlayerClick != null) OnPlayerClick(this, Button, Action, Yaw, Pitch, EntityID, X, Y, Z, face);
OnPlayerClickEvent.Call(this, Button, Action, Yaw, Pitch, EntityID, X, Y, Z, face);
}
+
+ void HandleTwoWayPing(byte[] packet) {
+ bool serverToClient = packet[1] != 0;
+ ushort data = NetUtils.ReadU16(packet, 2);
+
+ if (!serverToClient) {
+ // Client-> server ping, immediately send reply.
+ Send(Packet.TwoWayPing(false, data));
+ } else {
+ // Server -> client ping, set time received for reply.
+ Ping.Update(data);
+ }
+ }
void CheckReadAllExtensions() {
if (extensionCount <= 0 && !finishedCpeLogin) {
@@ -99,7 +112,7 @@ namespace MCGalaxy {
public static void SendMessage(Player p, string message, bool colorParse) {
if (p == null) {
- Logger.Log(LogType.ConsoleMessage, message);
+ Logger.Log(LogType.ConsoleMessage, message);
} else {
p.SendMessage(0, message, colorParse);
}
diff --git a/MCGalaxy/Network/Utils/PingList.cs b/MCGalaxy/Network/Utils/PingList.cs
new file mode 100644
index 000000000..4a8d815d6
--- /dev/null
+++ b/MCGalaxy/Network/Utils/PingList.cs
@@ -0,0 +1,96 @@
+/*
+ Copyright 2015 MCGalaxy
+
+ Dual-licensed under the Educational Community License, Version 2.0 and
+ the GNU General Public License, Version 3 (the "Licenses"); you may
+ not use this file except in compliance with the Licenses. You may
+ obtain a copy of the Licenses at
+
+ http://www.opensource.org/licenses/ecl2.php
+ http://www.gnu.org/licenses/gpl-3.0.html
+
+ Unless required by applicable law or agreed to in writing,
+ software distributed under the Licenses are distributed on an "AS IS"
+ BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ or implied. See the Licenses for the specific language governing
+ permissions and limitations under the Licenses.
+ */
+using System;
+
+namespace MCGalaxy.Network {
+ public sealed class PingList {
+
+ public struct PingEntry {
+ public DateTime TimeSent, TimeReceived;
+ public ushort Data;
+ }
+ public PingEntry[] Entries = new PingEntry[10];
+
+
+ public ushort NextTwoWayPingData() {
+ // Find free ping slot
+ for (int i = 0; i < Entries.Length; i++) {
+ if (Entries[i].TimeSent.Ticks != 0) continue;
+
+ ushort prev = i > 0 ? Entries[i - 1].Data : (ushort)0;
+ return SetTwoWayPing(i, prev);
+ }
+
+ // Remove oldest ping slot
+ for (int i = 0; i < Entries.Length - 1; i++) {
+ Entries[i] = Entries[i + 1];
+ }
+ int j = Entries.Length - 1;
+ return SetTwoWayPing(j, Entries[j].Data);
+ }
+
+ ushort SetTwoWayPing(int i, ushort prev) {
+ Entries[i].Data = (ushort)(prev + 1);
+ Entries[i].TimeSent = DateTime.UtcNow;
+ return (ushort)(prev + 1);
+ }
+
+ public void Update(ushort data) {
+ for (int i = 0; i < Entries.Length; i++ ) {
+ if (Entries[i].Data != data) continue;
+ Entries[i].TimeReceived = DateTime.UtcNow;
+ return;
+ }
+ }
+
+
+ /// Gets average ping in milliseconds, or 0 if no ping measures.
+ public double AveragePingMilliseconds() {
+ double totalMs = 0;
+ int measures = 0;
+
+ foreach (PingEntry ping in Entries) {
+ if (ping.TimeSent.Ticks == 0 || ping.TimeReceived.Ticks == 0) continue;
+
+ totalMs += (ping.TimeReceived - ping.TimeSent).TotalMilliseconds;
+ measures++;
+ }
+ return measures == 0 ? 0 : (totalMs / measures);
+ }
+
+
+ /// Gets worst ping in milliseconds, or 0 if no ping measures.
+ public double WorstPingMilliseconds() {
+ double totalMs = 0;
+
+ foreach (PingEntry ping in Entries) {
+ if (ping.TimeSent.Ticks == 0 || ping.TimeReceived.Ticks == 0) continue;
+
+ double ms = (ping.TimeReceived - ping.TimeSent).TotalMilliseconds;
+ totalMs = Math.Max(totalMs, ms);
+ }
+ return totalMs;
+ }
+
+ public string Format() {
+ return String.Format(" Worst ping {0}ms, average {1}ms",
+ WorstPingMilliseconds().ToString("N0"),
+ AveragePingMilliseconds().ToString("N0"));
+ }
+ }
+}
diff --git a/MCGalaxy/Player/Player.CPE.cs b/MCGalaxy/Player/Player.CPE.cs
index 9c63d8752..cbefdaab3 100644
--- a/MCGalaxy/Player/Player.CPE.cs
+++ b/MCGalaxy/Player/Player.CPE.cs
@@ -26,11 +26,11 @@ namespace MCGalaxy {
public int ChangeModel, EnvMapAppearance, EnvWeatherType, HackControl;
public int EmoteFix, MessageTypes, LongerMessages, FullCP437;
public int BlockDefinitions, BlockDefinitionsExt, TextColors, BulkBlockUpdate;
- public int EnvMapAspect, PlayerClick, EntityProperty, ExtEntityPositions;
+ public int EnvMapAspect, PlayerClick, EntityProperty, ExtEntityPositions, TwoWayPing;
- // these are checked frequently, so avoid overhead of HasCpeExt
+ // these are checked very frequently, so avoid overhead of HasCpeExt
public bool hasCustomBlocks, hasBlockDefs,
- hasTextColors, hasChangeModel, hasExtList, hasCP437;
+ hasTextColors, hasChangeModel, hasExtList, hasCP437, hasTwoWayPing;
public void AddExtension(string ext, int version) {
switch (ext.Trim()) {
@@ -96,6 +96,9 @@ namespace MCGalaxy {
case CpeExt.ExtEntityPositions:
ExtEntityPositions = version;
hasExtPositions = true; break;
+ case CpeExt.TwoWayPing:
+ TwoWayPing = version;
+ hasTwoWayPing = true; break;
}
}
@@ -126,6 +129,7 @@ namespace MCGalaxy {
case CpeExt.PlayerClick: return PlayerClick == version;
case CpeExt.EntityProperty: return EntityProperty == version;
case CpeExt.ExtEntityPositions: return ExtEntityPositions == version;
+ case CpeExt.TwoWayPing: return TwoWayPing == version;
default: return false;
}
}
@@ -253,6 +257,7 @@ namespace MCGalaxy {
public const string PlayerClick = "PlayerClick";
public const string EntityProperty = "EntityProperty";
public const string ExtEntityPositions = "ExtEntityPositions";
+ public const string TwoWayPing = "TwoWayPing";
}
public enum CpeMessageType : byte {
diff --git a/MCGalaxy/Player/Player.Fields.cs b/MCGalaxy/Player/Player.Fields.cs
index f04f9fd45..bc596fdcb 100644
--- a/MCGalaxy/Player/Player.Fields.cs
+++ b/MCGalaxy/Player/Player.Fields.cs
@@ -58,6 +58,7 @@ namespace MCGalaxy {
public string truename;
internal bool dontmindme = false;
INetworkSocket socket;
+ public PingList Ping = new PingList();
public DateTime LastAction, AFKCooldown;
public bool IsAfk = false, AutoAfk;
diff --git a/MCGalaxy/Player/Player.Handlers.cs b/MCGalaxy/Player/Player.Handlers.cs
index 4feea964e..60f7cb2a7 100644
--- a/MCGalaxy/Player/Player.Handlers.cs
+++ b/MCGalaxy/Player/Player.Handlers.cs
@@ -230,6 +230,7 @@ namespace MCGalaxy {
case Opcode.CpeCustomBlockSupportLevel: return 2;
case Opcode.CpePlayerClick: return 15;
case Opcode.Ping: return 1;
+ case Opcode.CpeTwoWayPing: return 4;
default:
if (!dontmindme) {
@@ -262,6 +263,8 @@ namespace MCGalaxy {
customBlockSupportLevel = buffer[1]; break;
case Opcode.CpePlayerClick:
HandlePlayerClicked(buffer); break;
+ case Opcode.CpeTwoWayPing:
+ HandleTwoWayPing(buffer); break;
}
}
diff --git a/MCGalaxy/Player/Player.Login.cs b/MCGalaxy/Player/Player.Login.cs
index edd72aad1..4f7502f1f 100644
--- a/MCGalaxy/Player/Player.Login.cs
+++ b/MCGalaxy/Player/Player.Login.cs
@@ -55,7 +55,7 @@ namespace MCGalaxy {
}
void SendCpeExtensions() {
- Send(Packet.ExtInfo(25), true);
+ Send(Packet.ExtInfo(26), true);
Send(Packet.ExtEntry(CpeExt.EnvMapAppearance, 1), true); // fix for classicube client, doesn't reply if only send EnvMapAppearance with version 2
Send(Packet.ExtEntry(CpeExt.ClickDistance, 1), true);
@@ -90,6 +90,7 @@ namespace MCGalaxy {
Send(Packet.ExtEntry(CpeExt.EntityProperty, 1), true);
Send(Packet.ExtEntry(CpeExt.ExtEntityPositions, 1), true);
+ Send(Packet.ExtEntry(CpeExt.TwoWayPing, 1), true);
}
void CompleteLoginProcess() {
diff --git a/MCGalaxy/Server/Tasks/ServerTasks.cs b/MCGalaxy/Server/Tasks/ServerTasks.cs
index e74671aba..d3f95e45f 100644
--- a/MCGalaxy/Server/Tasks/ServerTasks.cs
+++ b/MCGalaxy/Server/Tasks/ServerTasks.cs
@@ -95,7 +95,12 @@ namespace MCGalaxy.Tasks {
internal static void CheckState(SchedulerTask task) {
Player[] players = PlayerInfo.Online.Items;
foreach (Player p in players) {
- p.Send(Packet.Ping());
+ if (p.hasTwoWayPing) {
+ p.Send(Packet.TwoWayPing(true, p.Ping.NextTwoWayPingData()));
+ } else {
+ p.Send(Packet.Ping());
+ }
+
if (Server.afkminutes <= 0) return;
if (DateTime.UtcNow < p.AFKCooldown) return;