// Copyright 2014-2017 ClassicalSharp | Licensed under BSD-3 using System; using ClassicalSharp.Entities; using ClassicalSharp.Hotkeys; using ClassicalSharp.Map; using ClassicalSharp.Textures; using OpenTK.Input; #if USE16_BIT using BlockID = System.UInt16; #else using BlockID = System.Byte; #endif namespace ClassicalSharp.Network.Protocols { /// Implements the packets for classic protocol extension. public sealed class CPEProtocol : IProtocol { public CPEProtocol(Game game) : base(game) { } public override void Init() { Reset(); } public override void Reset() { if (!game.UseCPE) return; net.Set(Opcode.CpeExtInfo, HandleExtInfo, 67); net.Set(Opcode.CpeExtEntry, HandleExtEntry, 69); net.Set(Opcode.CpeSetClickDistance, HandleSetClickDistance, 3); net.Set(Opcode.CpeCustomBlockSupportLevel, HandleCustomBlockSupportLevel, 2); net.Set(Opcode.CpeHoldThis, HandleHoldThis, 3); net.Set(Opcode.CpeSetTextHotkey, HandleSetTextHotkey, 134); net.Set(Opcode.CpeExtAddPlayerName, HandleExtAddPlayerName, 196); net.Set(Opcode.CpeExtAddEntity, HandleExtAddEntity, 130); net.Set(Opcode.CpeExtRemovePlayerName, HandleExtRemovePlayerName, 3); net.Set(Opcode.CpeEnvColours, HandleEnvColours, 8); net.Set(Opcode.CpeMakeSelection, HandleMakeSelection, 86); net.Set(Opcode.CpeRemoveSelection, HandleRemoveSelection, 2); net.Set(Opcode.CpeSetBlockPermission, HandleSetBlockPermission, 4); net.Set(Opcode.CpeChangeModel, HandleChangeModel, 66); net.Set(Opcode.CpeEnvSetMapApperance, HandleEnvSetMapAppearance, 69); net.Set(Opcode.CpeEnvWeatherType, HandleEnvWeatherType, 2); net.Set(Opcode.CpeHackControl, HandleHackControl, 8); net.Set(Opcode.CpeExtAddEntity2, HandleExtAddEntity2, 138); net.Set(Opcode.CpeBulkBlockUpdate, HandleBulkBlockUpdate, 1282); net.Set(Opcode.CpeSetTextColor, HandleSetTextColor, 6); net.Set(Opcode.CpeSetMapEnvUrl, HandleSetMapEnvUrl, 65); net.Set(Opcode.CpeSetMapEnvProperty, HandleSetMapEnvProperty, 6); net.Set(Opcode.CpeSetEntityProperty, HandleSetEntityProperty, 7); } #region Read void HandleExtInfo() { string appName = reader.ReadString(); game.Chat.Add("Server software: " + appName); if (Utils.CaselessStarts(appName, "D3 server")) net.cpeData.needD3Fix = true; // Workaround for MCGalaxy that send ExtEntry sync but ExtInfoAsync. This means // ExtEntry may sometimes arrive before ExtInfo, and thus we have to use += instead of = net.cpeData.ServerExtensionsCount += reader.ReadInt16(); SendCpeExtInfoReply(); } void HandleExtEntry() { string extName = reader.ReadString(); int extVersion = reader.ReadInt32(); Utils.LogDebug("cpe ext: {0}, {1}", extName, extVersion); net.cpeData.HandleEntry(extName, extVersion, net); SendCpeExtInfoReply(); } void HandleSetClickDistance() { game.LocalPlayer.ReachDistance = reader.ReadUInt16() / 32f; } void HandleCustomBlockSupportLevel() { byte supportLevel = reader.ReadUInt8(); SendCustomBlockSupportLevel(1); game.UseCPEBlocks = true; if (supportLevel == 1) { game.Events.RaiseBlockPermissionsChanged(); } else { Utils.LogDebug("Server's block support level is {0}, this client only supports level 1.", supportLevel); Utils.LogDebug("You won't be able to see or use blocks from levels above level 1"); } } void HandleHoldThis() { BlockID block = reader.ReadUInt8(); if (block == Block.Air) block = Block.Invalid; bool canChange = reader.ReadUInt8() == 0; game.Inventory.CanChangeHeldBlock = true; game.Inventory.Selected = block; game.Inventory.CanChangeHeldBlock = canChange; } void HandleSetTextHotkey() { string label = reader.ReadString(); string action = reader.ReadString(); int keyCode = reader.ReadInt32(); byte keyMods = reader.ReadUInt8(); #if !ANDROID if (keyCode < 0 || keyCode > 255) return; Key key = LwjglToKey.Map[keyCode]; if (key == Key.Unknown) return; Utils.LogDebug("CPE Hotkey added: " + key + "," + keyMods + " : " + action); if (action == "") { game.Input.Hotkeys.RemoveHotkey(key, keyMods); } else if (action[action.Length - 1] == '◙') { // graphical form of \n action = action.Substring(0, action.Length - 1); game.Input.Hotkeys.AddHotkey(key, keyMods, action, false); } else { // more input needed by user game.Input.Hotkeys.AddHotkey(key, keyMods, action, true); } #endif } void HandleExtAddPlayerName() { short id = reader.ReadInt16(); string playerName = Utils.StripColours(reader.ReadString()); playerName = Utils.RemoveEndPlus(playerName); string listName = reader.ReadString(); listName = Utils.RemoveEndPlus(listName); string groupName = reader.ReadString(); byte groupRank = reader.ReadUInt8(); // Some server software will declare they support ExtPlayerList, but send AddEntity then AddPlayerName // we need to workaround this case by removing all the tab names we added for the AddEntity packets net.DisableAddEntityHack(); // Workaround for some servers that don't cast signed bytes to unsigned, before converting them to shorts. if (id < 0) id += 256; if (id >= 0 && id <= 255) net.AddTablistEntry((byte)id, playerName, listName, groupName, groupRank); } void HandleExtAddEntity() { byte id = reader.ReadUInt8(); string displayName = reader.ReadString(); string skinName = reader.ReadString(); net.CheckName(id, ref displayName, ref skinName); net.AddEntity(id, displayName, skinName, false); } void HandleExtRemovePlayerName() { short id = reader.ReadInt16(); // Workaround for some servers that don't cast signed bytes to unsigned, before converting them to shorts. if (id < 0) id += 256; if (id >= 0 && id <= 255) net.RemoveTablistEntry((byte)id); } void HandleMakeSelection() { byte selectionId = reader.ReadUInt8(); string label = reader.ReadString(); short startX = reader.ReadInt16(); short startY = reader.ReadInt16(); short startZ = reader.ReadInt16(); short endX = reader.ReadInt16(); short endY = reader.ReadInt16(); short endZ = reader.ReadInt16(); byte r = (byte)reader.ReadInt16(); byte g = (byte)reader.ReadInt16(); byte b = (byte)reader.ReadInt16(); byte a = (byte)reader.ReadInt16(); Vector3I p1 = Vector3I.Min(startX, startY, startZ, endX, endY, endZ); Vector3I p2 = Vector3I.Max(startX, startY, startZ, endX, endY, endZ); FastColour col = new FastColour(r, g, b, a); game.SelectionManager.AddSelection(selectionId, p1, p2, col); } void HandleRemoveSelection() { byte selectionId = reader.ReadUInt8(); game.SelectionManager.RemoveSelection(selectionId); } void HandleEnvColours() { byte variable = reader.ReadUInt8(); short red = reader.ReadInt16(); short green = reader.ReadInt16(); short blue = reader.ReadInt16(); bool invalid = red < 0 || red > 255 || green < 0 || green > 255 || blue < 0 || blue > 255; FastColour col = new FastColour(red, green, blue); if (variable == 0) { game.World.Env.SetSkyColour(invalid ? WorldEnv.DefaultSkyColour : col); } else if (variable == 1) { game.World.Env.SetCloudsColour(invalid ? WorldEnv.DefaultCloudsColour : col); } else if (variable == 2) { game.World.Env.SetFogColour(invalid ? WorldEnv.DefaultFogColour : col); } else if (variable == 3) { game.World.Env.SetShadowlight(invalid ? WorldEnv.DefaultShadowlight : col); } else if (variable == 4) { game.World.Env.SetSunlight(invalid ? WorldEnv.DefaultSunlight : col); } } void HandleSetBlockPermission() { byte blockId = reader.ReadUInt8(); bool canPlace = reader.ReadUInt8() != 0; bool canDelete = reader.ReadUInt8() != 0; Inventory inv = game.Inventory; if (blockId == 0) { int count = game.UseCPEBlocks ? Block.CpeCount : Block.OriginalCount; for (int i = 1; i < count; i++) { inv.CanPlace.SetNotOverridable(canPlace, i); inv.CanDelete.SetNotOverridable(canDelete, i); } } else { inv.CanPlace.SetNotOverridable(canPlace, blockId); inv.CanDelete.SetNotOverridable(canDelete, blockId); } game.Events.RaiseBlockPermissionsChanged(); } void HandleChangeModel() { byte playerId = reader.ReadUInt8(); string modelName = Utils.ToLower(reader.ReadString()); Entity entity = game.Entities[playerId]; if (entity != null) entity.SetModel(modelName); } void HandleEnvSetMapAppearance() { HandleSetMapEnvUrl(); game.World.Env.SetSidesBlock(reader.ReadUInt8()); game.World.Env.SetEdgeBlock(reader.ReadUInt8()); game.World.Env.SetEdgeLevel(reader.ReadInt16()); if (net.cpeData.envMapVer == 1) return; // Version 2 game.World.Env.SetCloudsLevel(reader.ReadInt16()); short maxViewDist = reader.ReadInt16(); game.MaxViewDistance = maxViewDist <= 0 ? 32768 : maxViewDist; game.SetViewDistance(game.UserViewDistance, false); } void HandleEnvWeatherType() { game.World.Env.SetWeather((Weather)reader.ReadUInt8()); } void HandleHackControl() { LocalPlayer p = game.LocalPlayer; p.Hacks.CanFly = reader.ReadUInt8() != 0; p.Hacks.CanNoclip = reader.ReadUInt8() != 0; p.Hacks.CanSpeed = reader.ReadUInt8() != 0; p.Hacks.CanRespawn = reader.ReadUInt8() != 0; p.Hacks.CanUseThirdPersonCamera = reader.ReadUInt8() != 0; p.CheckHacksConsistency(); float jumpHeight = reader.ReadInt16() / 32f; if (jumpHeight < 0) p.physics.jumpVel = p.Hacks.CanJumpHigher ? p.physics.userJumpVel : 0.42f; else p.physics.CalculateJumpVelocity(false, jumpHeight); p.physics.serverJumpVel = p.physics.jumpVel; game.Events.RaiseHackPermissionsChanged(); } void HandleExtAddEntity2() { byte id = reader.ReadUInt8(); string displayName = reader.ReadString(); string skinName = reader.ReadString(); net.CheckName(id, ref displayName, ref skinName); net.AddEntity(id, displayName, skinName, true); } const int bulkCount = 256; unsafe void HandleBulkBlockUpdate() { int count = reader.ReadUInt8() + 1; if (game.World.IsNotLoaded) { #if DEBUG_BLOCKS Utils.LogDebug("Server tried to update a block while still sending us the map!"); #endif reader.Skip(bulkCount * (sizeof(int) + 1)); return; } int* indices = stackalloc int[bulkCount]; for (int i = 0; i < count; i++) indices[i] = reader.ReadInt32(); reader.Skip((bulkCount - count) * sizeof(int)); for (int i = 0; i < count; i++) { BlockID block = reader.ReadUInt8(); Vector3I coords = game.World.GetCoords(indices[i]); if (coords.X < 0) { #if DEBUG_BLOCKS Utils.LogDebug("Server tried to update a block at an invalid position!"); #endif continue; } game.UpdateBlock(coords.X, coords.Y, coords.Z, block); } reader.Skip(bulkCount - count); } void HandleSetTextColor() { FastColour col = new FastColour(reader.ReadUInt8(), reader.ReadUInt8(), reader.ReadUInt8(), reader.ReadUInt8()); byte code = reader.ReadUInt8(); if (code <= ' ' || code > '~') return; // Control chars, space, extended chars cannot be used if (code == '%' || code == '&') return; // colour code signifiers cannot be used game.Drawer2D.Colours[code] = col; game.Events.RaiseColourCodeChanged((char)code); } void HandleSetMapEnvUrl() { string url = reader.ReadString(); if (!game.AllowServerTextures) return; if (url == "") { TexturePack.ExtractDefault(game); } else if (Utils.IsUrlPrefix(url, 0)) { net.RetrieveTexturePack(url); } Utils.LogDebug("Image url: " + url); } void HandleSetMapEnvProperty() { byte type = reader.ReadUInt8(); int value = reader.ReadInt32(); WorldEnv env = game.World.Env; Utils.Clamp(ref value, short.MinValue, short.MaxValue); switch (type) { case 0: Utils.Clamp(ref value, byte.MinValue, byte.MaxValue); env.SetSidesBlock((byte)value); break; case 1: Utils.Clamp(ref value, byte.MinValue, byte.MaxValue); env.SetEdgeBlock((byte)value); break; case 2: env.SetEdgeLevel(value); break; case 3: env.SetCloudsLevel(value); break; case 4: game.MaxViewDistance = value <= 0 ? 32768 : value; game.SetViewDistance(game.UserViewDistance, false); break; case 5: env.SetCloudsSpeed(value / 256f); break; case 6: env.SetWeatherSpeed(value / 256f); break; case 7: Utils.Clamp(ref value, byte.MinValue, byte.MaxValue); env.SetWeatherFade(value / 128f); break; case 8: env.SetExpFog(value != 0); break; } } void HandleSetEntityProperty() { byte id = reader.ReadUInt8(); byte type = reader.ReadUInt8(); int value = reader.ReadInt32(); Entity entity = game.Entities[id]; if (entity == null) return; LocationUpdate update = LocationUpdate.Empty(); switch (type) { case 0: update.RotX = LocationUpdate.Clamp(value); break; case 1: update.RotY = LocationUpdate.Clamp(value); break; case 2: update.RotZ = LocationUpdate.Clamp(value); break; default: return; } entity.SetLocation(update, true); } #endregion #region Write internal void SendPlayerClick(MouseButton button, bool buttonDown, byte targetId, PickedPos pos) { Player p = game.LocalPlayer; writer.WriteUInt8((byte)Opcode.CpePlayerClick); writer.WriteUInt8((byte)button); writer.WriteUInt8(buttonDown ? (byte)0 : (byte)1); writer.WriteInt16((short)Utils.DegreesToPacked(p.HeadY, 65536)); writer.WriteInt16((short)Utils.DegreesToPacked(p.HeadX, 65536)); writer.WriteUInt8(targetId); writer.WriteInt16((short)pos.BlockPos.X); writer.WriteInt16((short)pos.BlockPos.Y); writer.WriteInt16((short)pos.BlockPos.Z); writer.WriteUInt8((byte)pos.Face); net.SendPacket(); } internal void SendExtInfo(string appName, int extensionsCount) { writer.WriteUInt8((byte)Opcode.CpeExtInfo); writer.WriteString(appName); writer.WriteInt16((short)extensionsCount); net.SendPacket(); } internal void SendExtEntry(string extensionName, int extensionVersion) { writer.WriteUInt8((byte)Opcode.CpeExtEntry); writer.WriteString(extensionName); writer.WriteInt32(extensionVersion); net.SendPacket(); } internal void SendCustomBlockSupportLevel(byte version) { writer.WriteUInt8((byte)Opcode.CpeCustomBlockSupportLevel); writer.WriteUInt8(version); net.SendPacket(); } void SendCpeExtInfoReply() { if (net.cpeData.ServerExtensionsCount != 0) return; string[] clientExts = CPESupport.ClientExtensions; int count = clientExts.Length; if (!game.AllowCustomBlocks) count -= 2; SendExtInfo(net.AppName, count); for (int i = 0; i < clientExts.Length; i++) { string name = clientExts[i]; int ver = 1; if (name == "ExtPlayerList") ver = 2; if (name == "EnvMapAppearance") ver = net.cpeData.envMapVer; if (name == "BlockDefinitionsExt") ver = net.cpeData.blockDefsExtVer; if (!game.AllowCustomBlocks && name.StartsWith("BlockDefinitions")) continue; SendExtEntry(name, ver); } } #endregion } }