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;