ClassiCube/ClassicalSharp/Network/NetworkProcessor.cs

461 lines
14 KiB
C#

using System;
using System.Drawing;
using System.IO;
using System.Net;
using System.Net.Sockets;
using ClassicalSharp.Network;
using ClassicalSharp.TexturePack;
using OpenTK;
#if __MonoCS__
using Ionic.Zlib;
#else
using System.IO.Compression;
#endif
namespace ClassicalSharp {
public partial class NetworkProcessor : INetworkProcessor {
public NetworkProcessor( Game window ) {
game = window;
SetupHandlers();
}
public override bool IsSinglePlayer {
get { return false; }
}
Socket socket;
NetworkStream stream;
Game game;
bool receivedFirstPosition;
public override void Connect( IPAddress address, int port ) {
socket = new Socket( address.AddressFamily, SocketType.Stream, ProtocolType.Tcp );
try {
socket.Connect( address, port );
} catch( SocketException ex ) {
Utils.LogError( "Error while trying to connect: {0}{1}", Environment.NewLine, ex );
game.Disconnect( "&eUnable to reach " + address + ":" + port,
"Unable to establish an underlying connection" );
Dispose();
return;
}
stream = new NetworkStream( socket, true );
reader = new FastNetReader( stream );
gzippedMap = new FixedBufferStream( reader.buffer );
MakeLoginPacket( game.Username, game.Mppass );
SendPacket();
}
public override void SendChat( string text ) {
if( !String.IsNullOrEmpty( text ) ) {
MakeMessagePacket( text );
SendPacket();
}
}
public override void SendPosition( Vector3 pos, float yaw, float pitch ) {
byte payload = sendHeldBlock ? (byte)game.Inventory.HeldBlock : (byte)0xFF;
MakePositionPacket( pos, yaw, pitch, payload );
SendPacket();
}
public override void SendSetBlock( int x, int y, int z, bool place, byte block ) {
MakeSetBlockPacket( (short)x, (short)y, (short)z, place, block );
SendPacket();
}
public override void Dispose() {
socket.Close();
Disconnected = true;
}
void CheckForNewTerrainAtlas() {
DownloadedItem item;
game.AsyncDownloader.TryGetItem( "terrain", out item );
if( item != null && item.Data != null ) {
game.ChangeTerrainAtlas( (Bitmap)item.Data );
}
game.AsyncDownloader.TryGetItem( "texturePack", out item );
if( item != null && item.Data != null ) {
TexturePackExtractor extractor = new TexturePackExtractor();
extractor.Extract( (byte[])item.Data, game );
}
}
public override void Tick( double delta ) {
if( Disconnected ) return;
try {
reader.ReadPendingData();
} catch( IOException ex ) {
Utils.LogError( "Error while reading packets: {0}{1}", Environment.NewLine, ex );
game.Disconnect( "&eLost connection to the server", "Underlying connection was closed" );
Dispose();
return;
}
while( reader.size > 0 ) {
byte opcode = reader.buffer[0];
// Fix for older D3 servers which wrote one byte too many for HackControl packets.
if( opcode == 0xFF && lastOpcode == PacketId.CpeHackControl ) {
reader.Remove( 1 );
game.LocalPlayer.CalculateJumpVelocity( 1.4f ); // assume default jump height
continue;
}
if( reader.size < packetSizes[opcode] ) break;
ReadPacket( opcode );
}
Player player = game.LocalPlayer;
if( receivedFirstPosition ) {
SendPosition( player.Position, player.YawDegrees, player.PitchDegrees );
}
CheckForNewTerrainAtlas();
CheckForWomEnvironment();
}
readonly int[] packetSizes = {
131, 1, 1, 1028, 7, 9, 8, 74, 10, 7, 5, 4, 2,
66, 65, 2, 67, 69, 3, 2, 3, 134, 196, 130, 3,
8, 86, 2, 4, 66, 69, 2, 8, 138, 0, 77, 79, 2,
};
#region Writing
static byte[] outBuffer = new byte[131];
static int outIndex;
private static void MakeLoginPacket( string username, string verKey ) {
WriteUInt8( (byte)PacketId.Handshake );
WriteUInt8( 7 ); // protocol version
WriteString( username );
WriteString( verKey );
WriteUInt8( 0x42 );
}
private static void MakeSetBlockPacket( short x, short y, short z, bool place, byte block ) {
WriteUInt8( (byte)PacketId.SetBlockClient );
WriteInt16( x );
WriteInt16( y );
WriteInt16( z );
WriteUInt8( place ? (byte)1 : (byte)0 );
WriteUInt8( block );
}
private static void MakePositionPacket( Vector3 pos, float yaw, float pitch, byte payload ) {
WriteUInt8( (byte)PacketId.EntityTeleport );
WriteUInt8( payload ); // held block when using HeldBlock, otherwise just 255
WriteInt16( (short)( pos.X * 32 ) );
WriteInt16( (short)( (int)( pos.Y * 32 ) + 51 ) );
WriteInt16( (short)( pos.Z * 32 ) );
WriteUInt8( (byte)Utils.DegreesToPacked( yaw, 256 ) );
WriteUInt8( (byte)Utils.DegreesToPacked( pitch, 256 ) );
}
private static void MakeMessagePacket( string text ) {
WriteUInt8( (byte)PacketId.Message );
WriteUInt8( 0xFF ); // unused
WriteString( text );
}
static void WriteString( string value ) {
int count = Math.Min( value.Length, 64 );
for( int i = 0; i < count; i++ ) {
char c = value[i];
outBuffer[outIndex + i] = (byte)( c >= '\u0080' ? '?' : c );
}
for( int i = value.Length; i < 64; i++ ) {
outBuffer[outIndex + i] = (byte)' ';
}
outIndex += 64;
}
static void WriteUInt8( byte value ) {
outBuffer[outIndex++] = value;
}
static void WriteInt16( short value ) {
outBuffer[outIndex++] = (byte)( value >> 8 );
outBuffer[outIndex++] = (byte)( value );
}
static void WriteInt32( int value ) {
outBuffer[outIndex++] = (byte)( value >> 24 );
outBuffer[outIndex++] = (byte)( value >> 16 );
outBuffer[outIndex++] = (byte)( value >> 8 );
outBuffer[outIndex++] = (byte)( value );
}
void SendPacket() {
int packetLength = outIndex;
outIndex = 0;
if( Disconnected ) return;
try {
stream.Write( outBuffer, 0, packetLength );
} catch( IOException ex ) {
Utils.LogError( "Error while writing packets: {0}{1}", Environment.NewLine, ex );
game.Disconnect( "&eLost connection to the server", "Underlying connection was closed" );
Dispose();
}
}
#endregion
#region Reading
FastNetReader reader;
DateTime receiveStart;
DeflateStream gzipStream;
GZipHeaderReader gzipHeader;
int mapSizeIndex, mapIndex;
byte[] mapSize = new byte[4], map;
FixedBufferStream gzippedMap;
PacketId lastOpcode;
void ReadPacket( byte opcode ) {
reader.Remove( 1 ); // remove opcode
lastOpcode = (PacketId)opcode;
Action handler;
if( opcode >= maxHandledPacket || (handler = handlers[opcode]) == null)
throw new NotImplementedException( "Unsupported packet:" + (PacketId)opcode );
handler();
}
void HandleHandshake() {
byte protocolVer = reader.ReadUInt8();
ServerName = reader.ReadAsciiString();
ServerMotd = reader.ReadAsciiString();
game.LocalPlayer.SetUserType( reader.ReadUInt8() );
receivedFirstPosition = false;
game.LocalPlayer.ParseHackFlags( ServerName, ServerMotd );
}
void HandlePing() {
}
void HandleLevelInit() {
game.Map.Reset();
game.SetNewScreen( new LoadingMapScreen( game, ServerName, ServerMotd ) );
if( ServerMotd.Contains( "cfg=" ) ) {
ReadWomConfigurationAsync();
}
receivedFirstPosition = false;
gzipHeader = new GZipHeaderReader();
// Workaround because built in mono stream assumes that the end of stream
// has been reached the first time a read call returns 0. (MS.NET doesn't)
#if __MonoCS__
gzipStream = new DeflateStream( gzippedMap, true );
#else
gzipStream = new DeflateStream( gzippedMap, CompressionMode.Decompress );
if( OpenTK.Configuration.RunningOnMono ) {
Utils.LogWarning( "You are running on Mono, but this build does not support the Mono workaround." );
Utils.LogWarning( "You should either download the Mono compatible build or define '__MonoCS__' when targeting Mono. " +
"(The Mono compiler already defines this by default)" );
Utils.LogWarning( "You will likely experience an 'Internal error (no progress possible) ReadInternal' exception when decompressing the map." );
}
#endif
mapSizeIndex = 0;
mapIndex = 0;
receiveStart = DateTime.UtcNow;
}
void HandleLevelDataChunk() {
int usedLength = reader.ReadInt16();
gzippedMap.Position = 0;
gzippedMap.SetLength( usedLength );
if( gzipHeader.done || gzipHeader.ReadHeader( gzippedMap ) ) {
if( mapSizeIndex < 4 ) {
mapSizeIndex += gzipStream.Read( mapSize, mapSizeIndex, 4 - mapSizeIndex );
}
if( mapSizeIndex == 4 ) {
if( map == null ) {
int size = mapSize[0] << 24 | mapSize[1] << 16 | mapSize[2] << 8 | mapSize[3];
map = new byte[size];
}
mapIndex += gzipStream.Read( map, mapIndex, map.Length - mapIndex );
}
}
reader.Remove( 1024 );
byte progress = reader.ReadUInt8();
game.Events.RaiseMapLoading( progress );
}
void HandleLevelFinalise() {
game.SetNewScreen( new NormalScreen( game ) );
int mapWidth = reader.ReadInt16();
int mapHeight = reader.ReadInt16();
int mapLength = reader.ReadInt16();
double loadingMs = ( DateTime.UtcNow - receiveStart ).TotalMilliseconds;
Utils.LogDebug( "map loading took:" + loadingMs );
game.Map.UseRawMap( map, mapWidth, mapHeight, mapLength );
game.Events.RaiseOnNewMapLoaded();
map = null;
gzipStream.Dispose();
if( sendWomId && !sentWomId ) {
SendChat( "/womid WoMClient-2.0.7" );
sentWomId = true;
}
gzipStream = null;
GC.Collect();
}
void HandleSetBlock() {
int x = reader.ReadInt16();
int y = reader.ReadInt16();
int z = reader.ReadInt16();
byte type = reader.ReadUInt8();
if( game.Map.IsNotLoaded )
Utils.LogWarning( "Server tried to update a block while still sending us the map!" );
else if( !game.Map.IsValidPos( x, y, z ) )
Utils.LogWarning( "Server tried to update a block at an invalid position!" );
else
game.UpdateBlock( x, y, z, type );
}
void HandleAddEntity() {
byte entityId = reader.ReadUInt8();
string name = reader.ReadAsciiString();
AddEntity( entityId, name, name, true );
}
void HandleEntityTeleport() {
byte entityId = reader.ReadUInt8();
ReadAbsoluteLocation( entityId, true );
}
void HandleRelPosAndOrientationUpdate() {
byte playerId = reader.ReadUInt8();
float x = reader.ReadInt8() / 32f;
float y = reader.ReadInt8() / 32f;
float z = reader.ReadInt8() / 32f;
float yaw = (float)Utils.PackedToDegrees( reader.ReadUInt8() );
float pitch = (float)Utils.PackedToDegrees( reader.ReadUInt8() );
LocationUpdate update = LocationUpdate.MakePosAndOri( x, y, z, yaw, pitch, true );
UpdateLocation( playerId, update, true );
}
void HandleRelPositionUpdate() {
byte playerId = reader.ReadUInt8();
float x = reader.ReadInt8() / 32f;
float y = reader.ReadInt8() / 32f;
float z = reader.ReadInt8() / 32f;
LocationUpdate update = LocationUpdate.MakePos( x, y, z, true );
UpdateLocation( playerId, update, true );
}
void HandleOrientationUpdate() {
byte playerId = reader.ReadUInt8();
float yaw = (float)Utils.PackedToDegrees( reader.ReadUInt8() );
float pitch = (float)Utils.PackedToDegrees( reader.ReadUInt8() );
LocationUpdate update = LocationUpdate.MakeOri( yaw, pitch );
UpdateLocation( playerId, update, true );
}
void HandleRemoveEntity() {
byte entityId = reader.ReadUInt8();
Player player = game.Players[entityId];
if( entityId != 0xFF && player != null ) {
game.Events.RaiseEntityRemoved( entityId );
player.Despawn();
game.Players[entityId] = null;
}
}
void HandleMessage() {
byte messageType = reader.ReadUInt8();
string text = reader.ReadChatString( ref messageType, useMessageTypes );
game.Chat.Add( text, (CpeMessage)messageType );
}
void HandleKick() {
string reason = reader.ReadAsciiString();
game.Disconnect( "&eLost connection to the server", reason );
Dispose();
}
void HandleSetPermission() {
game.LocalPlayer.SetUserType( reader.ReadUInt8() );
}
void AddEntity( byte entityId, string displayName, string skinName, bool readPosition ) {
if( entityId != 0xFF ) {
Player oldPlayer = game.Players[entityId];
if( oldPlayer != null ) {
game.Events.RaiseEntityRemoved( entityId );
oldPlayer.Despawn();
}
game.Players[entityId] = new NetPlayer( displayName, skinName, game );
game.Events.RaiseEntityAdded( entityId );
game.AsyncDownloader.DownloadSkin( skinName );
}
if( readPosition ) {
ReadAbsoluteLocation( entityId, false );
if( entityId == 0xFF ) {
game.LocalPlayer.SpawnPoint = game.LocalPlayer.Position;
}
}
}
void ReadAbsoluteLocation( byte playerId, bool interpolate ) {
float x = reader.ReadInt16() / 32f;
float y = ( reader.ReadInt16() - 51 ) / 32f; // We have to do this.
if( playerId == 255 ) y += 22/32f;
float z = reader.ReadInt16() / 32f;
float yaw = (float)Utils.PackedToDegrees( reader.ReadUInt8() );
float pitch = (float)Utils.PackedToDegrees( reader.ReadUInt8() );
if( playerId == 0xFF ) {
receivedFirstPosition = true;
}
LocationUpdate update = LocationUpdate.MakePosAndOri( x, y, z, yaw, pitch, false );
UpdateLocation( playerId, update, interpolate );
}
void UpdateLocation( byte playerId, LocationUpdate update, bool interpolate ) {
Player player = game.Players[playerId];
if( player != null ) {
player.SetLocation( update, interpolate );
}
}
#endregion
Action[] handlers;
int maxHandledPacket;
void SetupHandlers() {
maxHandledPacket = (int)PacketId.Max;
handlers = new Action[] { HandleHandshake, HandlePing, HandleLevelInit,
HandleLevelDataChunk, HandleLevelFinalise, null, HandleSetBlock,
HandleAddEntity, HandleEntityTeleport, HandleRelPosAndOrientationUpdate,
HandleRelPositionUpdate, HandleOrientationUpdate, HandleRemoveEntity,
HandleMessage, HandleKick, HandleSetPermission,
HandleCpeExtInfo, HandleCpeExtEntry, HandleCpeSetClickDistance,
HandleCpeCustomBlockSupportLevel, HandleCpeHoldThis, null,
HandleCpeExtAddPlayerName, HandleCpeExtAddEntity, HandleCpeExtRemovePlayerName,
HandleCpeEnvColours, HandleCpeMakeSelection, HandleCpeRemoveSelection,
HandleCpeSetBlockPermission, HandleCpeChangeModel, HandleCpeEnvSetMapApperance,
HandleCpeEnvWeatherType, HandleCpeHackControl, HandleCpeExtAddEntity2,
null, HandleCpeDefineBlockOrLiquid, HandleCpeDefineBlockOrLiquid,
HandleCpeRemoveBlockDefinition
};
}
}
}