mirror of
https://github.com/ClassiCube/ClassiCube.git
synced 2025-10-05 03:55:57 -04:00
451 lines
15 KiB
C#
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() { 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
|
|
}
|
|
}
|