451 lines
15 KiB
C#

// 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 {
/// <summary> Implements the packets for classic protocol extension. </summary>
public sealed class CPEProtocol : IProtocol {
public CPEProtocol(Game game) : base(game) { }
public override void Init() {
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.ReadInt16() / 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.HeldBlock = 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 >= '0' && code <= '9') || (code >= 'a' && code <= 'f')
|| (code >= 'A' && code <= 'F')) return; // Standard 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
}
}