mirror of
https://github.com/ClassiCube/ClassiCube.git
synced 2025-10-06 12:37:28 -04:00
449 lines
14 KiB
C#
449 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;
|
|
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 ) {
|
|
ErrorHandler.LogError( "connecting to server", ex );
|
|
game.Disconnect( "&eUnable to reach " + address + ":" + port,
|
|
"Unable to establish an underlying connection" );
|
|
Dispose();
|
|
return;
|
|
}
|
|
|
|
NetworkStream stream = new NetworkStream( socket, true );
|
|
reader = new NetReader( stream );
|
|
writer = new NetWriter( stream );
|
|
gzippedMap = new FixedBufferStream( reader.buffer );
|
|
MakeLoginPacket( game.Username, game.Mppass );
|
|
SendPacket();
|
|
}
|
|
|
|
|
|
public override void Dispose() {
|
|
socket.Close();
|
|
Disconnected = true;
|
|
}
|
|
|
|
void CheckForNewTerrainAtlas() {
|
|
DownloadedItem item;
|
|
if( game.AsyncDownloader.TryGetItem( "terrain", out item ) ) {
|
|
if( item.Data != null ) {
|
|
game.ChangeTerrainAtlas( (Bitmap)item.Data );
|
|
} else {
|
|
TexturePackExtractor extractor = new TexturePackExtractor();
|
|
extractor.Extract( game.DefaultTexturePack, game );
|
|
}
|
|
}
|
|
|
|
if( game.AsyncDownloader.TryGetItem( "texturePack", out item ) ) {
|
|
TexturePackExtractor extractor = new TexturePackExtractor();
|
|
if( item.Data != null ) {
|
|
extractor.Extract( (byte[])item.Data, game );
|
|
} else {
|
|
extractor.Extract( game.DefaultTexturePack, game );
|
|
}
|
|
}
|
|
}
|
|
|
|
public override void Tick( double delta ) {
|
|
if( Disconnected ) return;
|
|
|
|
try {
|
|
reader.ReadPendingData();
|
|
} catch( IOException ex ) {
|
|
ErrorHandler.LogError( "reading packets", ex );
|
|
game.Disconnect( "&eLost connection to the server", "I/O error when reading packets" );
|
|
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, 80, 2,
|
|
};
|
|
|
|
#region Writing
|
|
NetWriter writer;
|
|
|
|
public override void SendChat( string text, bool partial ) {
|
|
if( String.IsNullOrEmpty( text ) ) return;
|
|
byte payload = !ServerSupportsPatialMessages ? (byte)0xFF:
|
|
partial ? (byte)1 : (byte)0;
|
|
|
|
writer.WriteUInt8( (byte)PacketId.Message );
|
|
writer.WriteUInt8( payload );
|
|
writer.WriteString( 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 ) {
|
|
writer.WriteUInt8( (byte)PacketId.SetBlockClient );
|
|
writer.WriteInt16( (short)x );
|
|
writer.WriteInt16( (short)y );
|
|
writer.WriteInt16( (short)z );
|
|
writer.WriteUInt8( place ? (byte)1 : (byte)0 );
|
|
writer.WriteUInt8( block );
|
|
SendPacket();
|
|
}
|
|
|
|
void MakeLoginPacket( string username, string verKey ) {
|
|
writer.WriteUInt8( (byte)PacketId.Handshake );
|
|
writer.WriteUInt8( 7 ); // protocol version
|
|
writer.WriteString( username );
|
|
writer.WriteString( verKey );
|
|
writer.WriteUInt8( 0x42 );
|
|
}
|
|
|
|
void MakePositionPacket( Vector3 pos, float yaw, float pitch, byte payload ) {
|
|
writer.WriteUInt8( (byte)PacketId.EntityTeleport );
|
|
writer.WriteUInt8( payload ); // held block when using HeldBlock, otherwise just 255
|
|
writer.WriteInt16( (short)(pos.X * 32) );
|
|
writer.WriteInt16( (short)((int)(pos.Y * 32) + 51) );
|
|
writer.WriteInt16( (short)(pos.Z * 32) );
|
|
writer.WriteUInt8( (byte)Utils.DegreesToPacked( yaw, 256 ) );
|
|
writer.WriteUInt8( (byte)Utils.DegreesToPacked( pitch, 256 ) );
|
|
}
|
|
|
|
void SendPacket() {
|
|
try {
|
|
writer.Send( Disconnected );
|
|
} catch( IOException ex ) {
|
|
ErrorHandler.LogError( "wrting packets", ex );
|
|
game.Disconnect( "&eLost connection to the server", "I/O Error while writing packets" );
|
|
Dispose();
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
|
|
#region Reading
|
|
|
|
NetReader 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 ) {
|
|
throw new InvalidOperationException( "You must compile ClassicalSharp with __MonoCS__ defined " +
|
|
"to run on Mono, due to a limitation in Mono." );
|
|
}
|
|
#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.SetData( map, mapWidth, mapHeight, mapLength );
|
|
game.Events.RaiseOnNewMapLoaded();
|
|
|
|
map = null;
|
|
gzipStream.Dispose();
|
|
if( sendWomId && !sentWomId ) {
|
|
SendChat( "/womid WoMClient-2.0.7", false );
|
|
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.LogDebug( "Server tried to update a block while still sending us the map!" );
|
|
else if( !game.Map.IsValidPos( x, y, z ) )
|
|
Utils.LogDebug( "Server tried to update a block at an invalid position!" );
|
|
else
|
|
game.UpdateBlock( x, y, z, type );
|
|
}
|
|
|
|
bool[] needRemoveNames;
|
|
void HandleAddEntity() {
|
|
byte entityId = reader.ReadUInt8();
|
|
string name = reader.ReadAsciiString();
|
|
name = Utils.RemoveEndPlus( name );
|
|
AddEntity( entityId, name, name, true );
|
|
|
|
// Some servers (such as LegendCraft) declare they support ExtPlayerList but
|
|
// don't send ExtAddPlayerName packets. So we add a special case here, even
|
|
// though it is technically against the specification.
|
|
if( UsingExtPlayerList ) {
|
|
AddCpeInfo( entityId, name, name, "Players", 0 );
|
|
if( needRemoveNames == null )
|
|
needRemoveNames = new bool[EntityList.MaxCount];
|
|
needRemoveNames[entityId] = 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;
|
|
}
|
|
|
|
// See comment about LegendCraft in AddEntity
|
|
if( needRemoveNames != null &&
|
|
needRemoveNames[entityId] && player != null ) {
|
|
game.Events.RaiseCpeListInfoRemoved( entityId );
|
|
game.CpePlayersList[entityId] = null;
|
|
needRemoveNames[entityId] = false;
|
|
}
|
|
}
|
|
|
|
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() {
|
|
handlers = new Action[] { HandleHandshake, HandlePing, HandleLevelInit,
|
|
HandleLevelDataChunk, HandleLevelFinalise, null, HandleSetBlock,
|
|
HandleAddEntity, HandleEntityTeleport, HandleRelPosAndOrientationUpdate,
|
|
HandleRelPositionUpdate, HandleOrientationUpdate, HandleRemoveEntity,
|
|
HandleMessage, HandleKick, HandleSetPermission,
|
|
|
|
HandleCpeExtInfo, HandleCpeExtEntry, HandleCpeSetClickDistance,
|
|
HandleCpeCustomBlockSupportLevel, HandleCpeHoldThis, HandleCpeSetTextHotkey,
|
|
HandleCpeExtAddPlayerName, HandleCpeExtAddEntity, HandleCpeExtRemovePlayerName,
|
|
HandleCpeEnvColours, HandleCpeMakeSelection, HandleCpeRemoveSelection,
|
|
HandleCpeSetBlockPermission, HandleCpeChangeModel, HandleCpeEnvSetMapApperance,
|
|
HandleCpeEnvWeatherType, HandleCpeHackControl, HandleCpeExtAddEntity2,
|
|
null, HandleCpeDefineBlock, HandleCpeRemoveBlockDefinition,
|
|
};
|
|
maxHandledPacket = handlers.Length;
|
|
}
|
|
}
|
|
} |