Delegate packet handling to IPacketFormat.

This commit is contained in:
Ray Koopa 2019-01-22 00:47:53 +01:00
parent cc84ed3d5f
commit a8073367f4
60 changed files with 876 additions and 543 deletions

View File

@ -0,0 +1,59 @@
using System.Collections.Generic;
using Syroot.Worms.Mgame.GameServer.Packets.OnlineWorms.Channel;
namespace Syroot.Worms.Mgame.GameServer
{
internal partial class Client
{
// ---- METHODS (PUBLIC) ---------------------------------------------------------------------------------------
public void HandleOWChannelConnect(LoginQuery packet)
{
SendPacket(new LoginReply
{
Result = ChannelConnectResult.Success,
Player = new ChannelConnectPlayerInfo
{
ID = packet.Players[0].UserName,
Name = "Your Name",
Experience = 1337,
Gold = 1000000,
Rank = 19,
GuildMarkIndex = 1,
GuildName = "Your Guild"
}
});
}
public void HandleOWChannelTop20(Top20Query packet)
{
// Send some simulated player rank infos.
Top20Reply reply = new Top20Reply
{
UnknownA = "Test",
Top20 = new List<ChannelTop20Player>(20)
};
for (int i = 0; i < 20; i++)
{
reply.Top20.Add(new ChannelTop20Player
{
Name = $"GoodPlayer{(char)('A' + i)}",
Rank = (ushort)((i + 6) / 3),
Experience = (ulong)(20 - i) * 957
});
}
SendPacket(reply);
// This is the last channel info packet, tell the client to go to the channel screen.
SendPacket(new EnterFinishReply());
}
public void HandleOWChannelStartSingleGameQuery(StartSingleGameQuery packet)
{
SendPacket(new StartSingleGameReply
{
Success = packet.RoundType == GameStartRoundType.First
});
}
}
}

View File

@ -1,8 +1,7 @@
using System.Collections.Generic;
using System.Drawing;
using System.Drawing;
using System.Net;
using Syroot.Worms.Mgame.GameServer.Packets.Data;
using Syroot.Worms.Mgame.GameServer.Packets.OnlineWorms;
using Syroot.Worms.Mgame.GameServer.Packets.OnlineWorms.Server;
namespace Syroot.Worms.Mgame.GameServer
{
@ -10,7 +9,7 @@ namespace Syroot.Worms.Mgame.GameServer
{
// ---- METHODS (PUBLIC) ---------------------------------------------------------------------------------------
public void HandleOWConnect(ConnectQuery packet)
public void HandleOWServerConnect(ConnectQuery packet)
{
SendPacket(new ConnectReply
{
@ -20,7 +19,7 @@ namespace Syroot.Worms.Mgame.GameServer
});
}
public void HandleOWLogin(LoginQuery packet)
public void HandleOWServerLogin(LoginQuery packet)
{
// Send login result.
// Create player infos from the given credentials. This would be the place to check for actual accounts.
@ -37,7 +36,7 @@ namespace Syroot.Worms.Mgame.GameServer
});
// Send info text.
SendPacket(new ServerInfoReply
SendPacket(new InfoReply
{
Text = "Welcome to the Online Worms Server."
});
@ -96,7 +95,7 @@ namespace Syroot.Worms.Mgame.GameServer
});
}
public void HandleOWChannelEnter(ChannelEnterQuery packet)
public void HandleOWServerChannelEnter(ChannelEnterQuery packet)
{
// Simply allow joining a channel running on the same server port.
SendPacket(new ChannelEnterReply
@ -104,54 +103,5 @@ namespace Syroot.Worms.Mgame.GameServer
EndPoint = _server.Config.EndPoint
});
}
public void HandleOWChannelConnect(ChannelLoginQuery packet)
{
SendPacket(new ChannelLoginReply
{
Result = ChannelConnectResult.Success,
Player = new ChannelConnectPlayerInfo
{
ID = packet.Players[0].UserName,
Name = "Your Name",
Experience = 1337,
Gold = 1000000,
Rank = 19,
GuildMarkIndex = 1,
GuildName = "Your Guild"
}
});
}
public void HandleOWChannelTop20(ChannelTop20Query packet)
{
// Send some simulated player rank infos.
ChannelTop20Reply reply = new ChannelTop20Reply
{
UnknownA = "Test",
Top20 = new List<ChannelTop20Player>(20)
};
for (int i = 0; i < 20; i++)
{
reply.Top20.Add(new ChannelTop20Player
{
Name = $"GoodPlayer{(char)('A' + i)}",
Rank = (ushort)((i + 6) / 3),
Experience = (ulong)(20 - i) * 957
});
}
SendPacket(reply);
// This is the last channel info packet, tell the client to go to the channel screen.
SendPacket(new ChannelEnterFinishReply());
}
public void HandleOWStartSingleGameQuery(StartSingleGameQuery packet)
{
SendPacket(new StartSingleGameReply
{
Success = packet.RoundType == GameStartRoundType.First
});
}
}
}

View File

@ -1,4 +1,5 @@
using System.Collections.Generic;
using Syroot.Worms.Mgame.GameServer.Packets.WorldPartyAqua;
using Syroot.Worms.Mgame.GameServer.Packets.WorldPartyAqua.Server;
namespace Syroot.Worms.Mgame.GameServer
@ -107,5 +108,7 @@ namespace Syroot.Worms.Mgame.GameServer
{
SendPacket(new OptionsSaveReply { Success = true });
}
public void HandleWwpaRawPacket(RawPacket packet) { }
}
}

View File

@ -4,13 +4,15 @@ using System.Net.Sockets;
using System.Threading;
using Syroot.Worms.Mgame.GameServer.Core;
using Syroot.Worms.Mgame.GameServer.Packets;
using Syroot.Worms.Mgame.GameServer.Packets.OnlineWorms;
using Syroot.Worms.Mgame.GameServer.Packets.WorldPartyAqua;
namespace Syroot.Worms.Mgame.GameServer
{
/// <summary>
/// Represents a connection with an Online Worms client which replies to received packets appropriately.
/// Represents a connection with a game client which replies to received packets appropriately.
/// </summary>
internal partial class Client : AppConnection
internal partial class Client : PacketConnection
{
// ---- FIELDS -------------------------------------------------------------------------------------------------
@ -24,25 +26,15 @@ namespace Syroot.Worms.Mgame.GameServer
/// </summary>
/// <param name="tcpClient">The <see cref="TcpClient"/> representing the connection to the client.</param>
/// <param name="server">The <see cref="Server"/> instance with which this client communicates.</param>
internal Client(TcpClient tcpClient, Server server)
: base(tcpClient)
internal Client(TcpClient tcpClient, Server server) : base(tcpClient,
WwpaPacketFormat.Instance, OWChannelPacketFormat.Instance, OWServerPacketFormat.Instance)
{
_server = server;
PrePacketHandle += (s, e) => _server.Log.Write(LogCategory.Client, FormatPacket(e, ">>"));
PrePacketSend += (s, e) =>
{
_server.Log.Write(LogCategory.Server, FormatPacket(e, "<<"));
if (_server.Config.SendDelay > 0)
Thread.Sleep(_server.Config.SendDelay);
};
PrePacketHandle += OnPrePacketHandle;
PrePacketSend += OnPrePacketSend;
UnhandledException += OnUnhandledException;
}
#if DEBUG
// ---- METHODS (PUBLIC) ---------------------------------------------------------------------------------------
public void HandleRawPacket(RawPacket packet) { }
#endif
// ---- METHODS (PRIVATE) --------------------------------------------------------------------------------------
private string FormatPacket(IPacket packet, string direction)
@ -50,5 +42,24 @@ namespace Syroot.Worms.Mgame.GameServer
string packetName = String.Join(' ', packet.GetType().FullName.Split('.').TakeLast(2));
return $"{TcpClient.Client.RemoteEndPoint} {direction} {packetName}{ObjectDumper.Dump(packet)}";
}
// ---- EVENTHANDLERS ------------------------------------------------------------------------------------------
private void OnPrePacketHandle(object sender, IPacket packet)
{
_server.Log.Write(LogCategory.Client, FormatPacket(packet, ">>"));
}
private void OnPrePacketSend(object sender, IPacket packet)
{
_server.Log.Write(LogCategory.Server, FormatPacket(packet, "<<"));
if (_server.Config.SendDelay > 0)
Thread.Sleep(_server.Config.SendDelay);
}
private void OnUnhandledException(object sender, Exception ex)
{
_server.Log.Write(LogCategory.Error, $"{TcpClient.Client.RemoteEndPoint} {ex.Message}");
}
}
}

View File

@ -0,0 +1,63 @@
using System;
using System.IO;
namespace Syroot.Worms.Mgame.GameServer.Core.IO
{
/// <summary>
/// Represents a <see cref="Stream"/> which caches the last bytes in a buffer and allows virtual back-seeking even
/// in non-seekable streams.
/// </summary>
/// <typeparam name="T">The type of the wrapped <see cref="Stream"/>.</typeparam>
internal class CachedStream<T> : WrapperStream<T>
where T : Stream
{
// ---- FIELDS -------------------------------------------------------------------------------------------------
private readonly byte[] _cache;
private int _virtualPosition;
private int _cacheEnd;
// ---- CONSTRUCTORS & DESTRUCTOR ------------------------------------------------------------------------------
internal CachedStream(T baseStream, int cacheSize, bool leaveOpen = false) : base(baseStream, leaveOpen)
{
_cache = new byte[cacheSize];
}
// ---- METHODS (PUBLIC) ---------------------------------------------------------------------------------------
public override int Read(byte[] buffer, int offset, int count)
{
// Return bytes from cache first, if possible.
int cacheBytes = Math.Min(_cacheEnd - _virtualPosition, count);
if (cacheBytes > 0)
{
Buffer.BlockCopy(_cache, _virtualPosition, buffer, offset, cacheBytes);
_virtualPosition += cacheBytes;
offset += cacheBytes;
}
// Read more bytes from the stream if the cache is not covering all.
int streamBytes = count - cacheBytes;
if (streamBytes > 0)
{
if (_cache.Length - _cacheEnd < streamBytes)
throw new IOException("Cache cannot hold more bytes.");
streamBytes = BaseStream.Read(_cache, _cacheEnd, streamBytes);
_cacheEnd += streamBytes;
Buffer.BlockCopy(_cache, _virtualPosition, buffer, offset, count);
_virtualPosition += streamBytes;
}
return cacheBytes + streamBytes;
}
// ---- METHODS (INTERNAL) -------------------------------------------------------------------------------------
internal void MoveToCacheStart()
{
_virtualPosition = 0;
}
}
}

View File

@ -12,7 +12,7 @@ namespace Syroot.Worms.Mgame.GameServer.Core.IO
{
// ---- CONSTRUCTORS & DESTRUCTOR ------------------------------------------------------------------------------
internal SafeWrapperStream(T baseStream) : base(baseStream) { }
internal SafeWrapperStream(T baseStream, bool leaveOpen = false) : base(baseStream, leaveOpen) { }
// ---- METHODS (PUBLIC) ---------------------------------------------------------------------------------------

View File

@ -9,11 +9,23 @@ namespace Syroot.Worms.Mgame.GameServer.Core.IO
internal class WrapperStream<T> : Stream
where T : Stream
{
// ---- FIELDS -------------------------------------------------------------------------------------------------
private readonly bool _leaveOpen;
private bool _disposed;
// ---- CONSTRUCTORS & DESTRUCTOR ------------------------------------------------------------------------------
internal WrapperStream(T baseStream)
/// <summary>
/// Initializes a new instance of the <see cref="WrapperStream{T}"/> class wrapping the given
/// <paramref name="baseStream"/> which it may <paramref name="leaveOpen"/> on dispose.
/// </summary>
/// <param name="baseStream">The <see cref="Stream"/> to wrap.</param>
/// <param name="leaveOpen"><see langword="true"/> to leave the stream open on <see cref="Dispose()"/>.</param>
internal WrapperStream(T baseStream, bool leaveOpen = false)
{
BaseStream = baseStream;
_leaveOpen = leaveOpen;
}
// ---- PROPERTIES ---------------------------------------------------------------------------------------------
@ -40,5 +52,17 @@ namespace Syroot.Worms.Mgame.GameServer.Core.IO
public override long Seek(long offset, SeekOrigin origin) => BaseStream.Seek(offset, origin);
public override void SetLength(long value) => BaseStream.SetLength(value);
public override void Write(byte[] buffer, int offset, int count) => BaseStream.Write(buffer, offset, count);
// ---- METHODS (PROTECTED) ------------------------------------------------------------------------------------
protected override void Dispose(bool disposing)
{
if (_disposed)
return;
if (disposing && !_leaveOpen)
BaseStream.Dispose();
_disposed = true;
base.Dispose(disposing);
}
}
}

View File

@ -0,0 +1,24 @@
using System;
using System.Collections.Generic;
using System.Reflection;
namespace Syroot.Worms.Mgame.GameServer.Core.Reflection
{
/// <summary>
/// Represents utility methods helping with reflection.
/// </summary>
internal static class ReflectionTools
{
// ---- METHODS (INTERNAL) -------------------------------------------------------------------------------------
internal static IList<(Type type, TAttrib attrib)> GetAttributedClasses<TAttrib>()
where TAttrib : Attribute
{
List<(Type, TAttrib)> classes = new List<(Type, TAttrib)>();
foreach (Type type in Assembly.GetExecutingAssembly().GetTypes())
foreach (TAttrib attrib in type.GetCustomAttributes<TAttrib>())
classes.Add((type, attrib));
return classes;
}
}
}

View File

@ -34,6 +34,7 @@ namespace Syroot.Worms.Mgame.GameServer
internal enum LogCategory
{
Info = ConsoleColor.White,
Error = ConsoleColor.Red,
Connect = ConsoleColor.Cyan,
Disconnect = ConsoleColor.Magenta,
Client = ConsoleColor.DarkCyan,

View File

@ -1,271 +0,0 @@
using System;
using System.IO;
using System.Net.Sockets;
using Syroot.BinaryData;
using Syroot.BinaryData.Memory;
using Syroot.Worms.Mgame.GameServer.Core.Net;
using Syroot.Worms.Mgame.GameServer.Core.Reflection;
namespace Syroot.Worms.Mgame.GameServer.Packets
{
/// <summary>
/// Represents a class capable of dispatching received <see cref="IPacket"/> instances to a corresponding method.
/// </summary>
internal class AppConnection : IDisposable
{
// ---- CONSTANTS ----------------------------------------------------------------------------------------------
private const int _maxDataSize = 1024;
private const ushort _owTagStartChannel = 0xFFFE;
private const ushort _owTagEndChannel = 0xFEFF;
private const ushort _wwpaTagStart = 0x2E9E;
private const ushort _wwpaTagEnd = 0x7F3F;
// ---- FIELDS -------------------------------------------------------------------------------------------------
private readonly MethodHandler<IPacket> _methodHandler;
private readonly SafeNetworkStream _tcpStream;
private bool _disposed;
// ---- CONSTRUCTORS & DESTRUCTOR ------------------------------------------------------------------------------
/// <summary>
/// Initializes a new instance of the <see cref="AppConnection"/> class, handling the given
/// <paramref name="tcpClient"/>.
/// </summary>
/// <param name="tcpClient">The <see cref="System.Net.Sockets.TcpClient"/> to communicate with.</param>
internal AppConnection(TcpClient tcpClient)
{
_methodHandler = new MethodHandler<IPacket>(this);
TcpClient = tcpClient;
_tcpStream = TcpClient.GetSafeStream();
}
// ---- EVENTS -------------------------------------------------------------------------------------------------
protected event EventHandler<IPacket> PrePacketHandle;
protected event EventHandler<IPacket> PrePacketSend;
// ---- PROPERTIES ---------------------------------------------------------------------------------------------
/// <summary>
/// Gets or sets the <see cref="TcpClient"/> with which the connection communicates.
/// </summary>
internal TcpClient TcpClient { get; }
// ---- METHODS (PUBLIC) ---------------------------------------------------------------------------------------
public void Dispose()
{
// Do not change this code. Put cleanup code in Dispose(bool disposing).
Dispose(true);
}
// ---- METHODS (INTERNAL) -------------------------------------------------------------------------------------
/// <summary>
/// Starts handling packets incoming from this connection and dispatches them to corresponding methods.
/// </summary>
internal void Listen()
{
IPacket packet;
while ((packet = ReceivePacket()) != null)
{
PrePacketHandle?.Invoke(this, packet);
_methodHandler.Handle(packet);
}
}
// ---- METHODS (PROTECTED) ------------------------------------------------------------------------------------
protected virtual void Dispose(bool disposing)
{
if (!_disposed)
{
if (disposing)
TcpClient.Dispose();
_disposed = true;
}
}
protected bool SendPacket(IPacket packet)
{
PrePacketSend?.Invoke(this, packet);
try
{
// Handle specific packet formats.
PacketAttribute attribute = PacketFactory.GetAttribute(packet);
switch (attribute.Format)
{
case PacketFormat.OWChannel:
SendOWChannelPacket(attribute.ID, packet);
break;
case PacketFormat.OWServer:
SendOWServerPacket(attribute.ID, packet);
break;
case PacketFormat.Wwpa:
SendWwpaPacket(attribute.ID, packet, false);
break;
default:
throw new IOException("Cannot send unknown packet format.");
}
return true;
}
catch (IOException) { return false; } // A network error appeared, and communication should end.
catch (ObjectDisposedException) { return false; } // The underlying stream was most apparently closed.
}
// ---- METHODS (PRIVATE) --------------------------------------------------------------------------------------
private IPacket ReceivePacket()
{
try
{
// Handle specific packet formats.
ushort tag = _tcpStream.ReadUInt16();
switch (tag)
{
case _owTagStartChannel:
return ReceiveOWChannelPacket();
case _wwpaTagStart:
return ReceiveWwpaPacket();
default:
return ReceiveOWServerPacket(tag);
}
}
catch (IOException) { return null; } // The underlying socket closed or sent invalid data.
catch (ObjectDisposedException) { return null; } // The underlying stream closed.
}
private IPacket ReceiveOWChannelPacket()
{
// Read head.
if (_tcpStream.Read1Byte() != 1)
throw new IOException("Invalid OW channel packet start tag.");
int dataSize = _tcpStream.ReadUInt16();
int id = _tcpStream.Read1Byte();
// Read data.
SpanReader reader = new SpanReader(_tcpStream.ReadBytes(dataSize), encoding: Encodings.Korean);
// Read tail.
ushort endTag = _tcpStream.ReadUInt16();
if (endTag != _owTagEndChannel)
throw new IOException("Invalid OW channel packet end tag.");
// Instantiate, deserialize, and return packet.
IPacket packet = PacketFactory.Create(PacketFormat.OWChannel, id);
packet.Load(ref reader);
return packet;
}
private IPacket ReceiveOWServerPacket(int tag)
{
// Read head.
int dataSize = _tcpStream.ReadUInt16();
// Read data.
SpanReader reader = new SpanReader(_tcpStream.ReadBytes(dataSize), encoding: Encodings.Korean);
// Instantiate, deserialize, and return packet.
IPacket packet = PacketFactory.Create(PacketFormat.OWServer, tag);
packet.Load(ref reader);
return packet;
}
private IPacket ReceiveWwpaPacket()
{
// Read head.
if (!_tcpStream.ReadBoolean())
throw new IOException("Unexpected WWPA packet bHead2.");
if (_tcpStream.ReadBoolean())
throw new IOException("Unexpected WWPA packet bHead3.");
if (!_tcpStream.ReadBoolean())
throw new IOException("Unexpected WWPA packet bIsCompressed.");
int decompressedSize = _tcpStream.ReadUInt16();
int compressedSize = _tcpStream.ReadInt32();
int idxPacket = _tcpStream.ReadInt32();
// Read data.
byte[] compressedData = _tcpStream.ReadBytes(compressedSize);
// Read tail.
if (_tcpStream.ReadInt32() != idxPacket)
throw new IOException("Invalid WWPA packet index.");
if (_tcpStream.ReadUInt16() != _wwpaTagEnd)
throw new IOException("Invalid WWPA packet end tag.");
// Instantiate, deserialize, and return packet.
SpanReader reader = new SpanReader(PacketCompression.Decompress(compressedData), encoding: Encodings.Korean);
int id = reader.ReadInt32();
IPacket packet = PacketFactory.Create(PacketFormat.Wwpa, id);
SpanReader dataReader = reader.Slice();
packet.Load(ref dataReader);
return packet;
}
private void SendOWChannelPacket(int id, IPacket packet)
{
// Retrieve data. Must have at least 1 byte.
SpanWriter writer = new SpanWriter(new byte[_maxDataSize], encoding: Encodings.Korean);
packet.Save(ref writer);
writer.Position = Math.Max(1, writer.Position);
// Send head.
_tcpStream.WriteUInt16(_owTagStartChannel);
_tcpStream.WriteByte(1);
_tcpStream.WriteUInt16((ushort)writer.Position);
_tcpStream.WriteByte((byte)id);
// Send data.
_tcpStream.Write(writer.Span);
// Send tail.
_tcpStream.WriteUInt16(_owTagEndChannel);
}
private void SendOWServerPacket(int id, IPacket packet)
{
// Retrieve data.
SpanWriter writer = new SpanWriter(new byte[_maxDataSize], encoding: Encodings.Korean);
packet.Save(ref writer);
// Send head.
_tcpStream.WriteUInt16((ushort)id);
_tcpStream.WriteUInt16((ushort)writer.Position);
// Write data.
_tcpStream.Write(writer.Span);
}
private void SendWwpaPacket(int id, IPacket packet, bool compress)
{
// Retrieve (decompressed) data.
SpanWriter writer = new SpanWriter(new byte[_maxDataSize], encoding: Encodings.Korean);
writer.WriteInt32(id);
SpanWriter dataWriter = writer.Slice();
packet.Save(ref dataWriter);
writer.Position += dataWriter.Position;
// Send head and data.
_tcpStream.WriteUInt16(_wwpaTagStart);
_tcpStream.WriteBoolean(true); // bHead2 (unknown)
_tcpStream.WriteBoolean(false); // bHead3 (unknown)
_tcpStream.WriteBoolean(compress);
if (compress)
{
ReadOnlySpan<byte> compressedData = PacketCompression.Compress(writer.Span);
_tcpStream.WriteUInt16((ushort)writer.Position); // decompressed size
_tcpStream.WriteUInt32((uint)compressedData.Length); // compressed size
_tcpStream.WriteInt32(1); // index
_tcpStream.Write(compressedData);
}
else
{
_tcpStream.WriteUInt16(0); // unused
_tcpStream.WriteUInt32((ushort)writer.Position); // raw size
_tcpStream.WriteInt32(1); // index
_tcpStream.Write(writer.Span.Slice(0, writer.Position));
}
// Send tail.
_tcpStream.WriteInt32(1); // index (must equal other)
_tcpStream.WriteUInt16(_wwpaTagEnd);
}
}
}

View File

@ -1,14 +1,14 @@
using System;
using Syroot.BinaryData.Memory;
namespace Syroot.Worms.Mgame.GameServer.Packets.OnlineWorms
namespace Syroot.Worms.Mgame.GameServer.Packets.OnlineWorms.Channel
{
/// <summary>
/// Represents an additional server response to a <see cref="ChannelTop20Query"/>, causing the client to switch
/// Represents an additional server response to a <see cref="Top20Query"/>, causing the client to switch
/// to the channel screen (game lobby).
/// </summary>
[Packet(PacketFormat.OWChannel, 0x44)]
internal class ChannelEnterFinishReply : IPacket
[OWChannelPacket(0x44)]
internal class EnterFinishReply : IPacket
{
// ---- METHODS (PUBLIC) ---------------------------------------------------------------------------------------

View File

@ -4,13 +4,13 @@ using System.Net;
using Syroot.BinaryData.Memory;
using Syroot.Worms.Mgame.GameServer.Packets.Data;
namespace Syroot.Worms.Mgame.GameServer.Packets.OnlineWorms
namespace Syroot.Worms.Mgame.GameServer.Packets.OnlineWorms.Channel
{
/// <summary>
/// Represents the client request for a <see cref="ChannelLoginReply"/>.
/// Represents the client request for a <see cref="LoginReply"/>.
/// </summary>
[Packet(PacketFormat.OWChannel, 0x10)]
internal class ChannelLoginQuery : IPacket
[OWChannelPacket(0x10)]
internal class LoginQuery : IPacket
{
// ---- PROPERTIES ---------------------------------------------------------------------------------------------

View File

@ -1,13 +1,13 @@
using System;
using Syroot.BinaryData.Memory;
namespace Syroot.Worms.Mgame.GameServer.Packets.OnlineWorms
namespace Syroot.Worms.Mgame.GameServer.Packets.OnlineWorms.Channel
{
/// <summary>
/// Represents the server response to a <see cref="ChannelLoginQuery"/>.
/// Represents the server response to a <see cref="LoginQuery"/>.
/// </summary>
[Packet(PacketFormat.OWChannel, 0x11)]
internal class ChannelLoginReply : IPacket
[OWChannelPacket(0x11)]
internal class LoginReply : IPacket
{
// ---- PROPERTIES ---------------------------------------------------------------------------------------------

View File

@ -1,12 +1,12 @@
using System;
using Syroot.BinaryData.Memory;
namespace Syroot.Worms.Mgame.GameServer.Packets.OnlineWorms
namespace Syroot.Worms.Mgame.GameServer.Packets.OnlineWorms.Channel
{
/// <summary>
/// Represents the client request for a <see cref="StartSingleGameReply"/>.
/// </summary>
[Packet(PacketFormat.OWChannel, 0x38)]
[OWChannelPacket(0x38)]
internal class StartSingleGameQuery : IPacket
{
// ---- PROPERTIES ---------------------------------------------------------------------------------------------

View File

@ -1,12 +1,12 @@
using System;
using Syroot.BinaryData.Memory;
namespace Syroot.Worms.Mgame.GameServer.Packets.OnlineWorms
namespace Syroot.Worms.Mgame.GameServer.Packets.OnlineWorms.Channel
{
/// <summary>
/// Represents the server response to a <see cref="StartSingleGameQuery"/>.
/// </summary>
[Packet(PacketFormat.OWChannel, 0x39)]
[OWChannelPacket(0x39)]
internal class StartSingleGameReply : IPacket
{
// ---- PROPERTIES ---------------------------------------------------------------------------------------------

View File

@ -1,13 +1,13 @@
using System;
using Syroot.BinaryData.Memory;
namespace Syroot.Worms.Mgame.GameServer.Packets.OnlineWorms
namespace Syroot.Worms.Mgame.GameServer.Packets.OnlineWorms.Channel
{
/// <summary>
/// Represents the client request for a <see cref="ChannelLoginReply"/>.
/// Represents the client request for a <see cref="LoginReply"/>.
/// </summary>
[Packet(PacketFormat.OWChannel, 0x37)]
internal class ChannelTop20Query : IPacket
[OWChannelPacket(0x37)]
internal class Top20Query : IPacket
{
// ---- PROPERTIES ---------------------------------------------------------------------------------------------

View File

@ -2,13 +2,13 @@
using System.Collections.Generic;
using Syroot.BinaryData.Memory;
namespace Syroot.Worms.Mgame.GameServer.Packets.OnlineWorms
namespace Syroot.Worms.Mgame.GameServer.Packets.OnlineWorms.Channel
{
/// <summary>
/// Represents the server response to a <see cref="ChannelTop20Query"/>.
/// Represents the server response to a <see cref="Top20Query"/>.
/// </summary>
[Packet(PacketFormat.OWChannel, 0x36)]
internal class ChannelTop20Reply : IPacket
[OWChannelPacket(0x36)]
internal class Top20Reply : IPacket
{
// ---- PROPERTIES ---------------------------------------------------------------------------------------------

View File

@ -0,0 +1,30 @@
using System;
namespace Syroot.Worms.Mgame.GameServer.Packets.OnlineWorms
{
/// <summary>
/// Decorates an <see cref="IPacket"/> child class with the ID of the OW channel packet it represents.
/// </summary>
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)]
internal class OWChannelPacketAttribute : Attribute
{
// ---- CONSTRUCTORS & DESTRUCTOR ------------------------------------------------------------------------------
/// <summary>
/// Initializes a new instance of the <see cref="OWChannelPacketAttribute"/> class, decorating a class with the
/// ID of the packet it represents.
/// </summary>
/// <param name="id">The ID of the packet which the decorated class represents.</param>
internal OWChannelPacketAttribute(byte id)
{
ID = id;
}
// ---- PROPERTIES ---------------------------------------------------------------------------------------------
/// <summary>
/// Gets the ID of the packet the decorated class represents.
/// </summary>
internal byte ID { get; }
}
}

View File

@ -0,0 +1,98 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Syroot.BinaryData;
using Syroot.BinaryData.Memory;
using Syroot.Worms.Mgame.GameServer.Core.Reflection;
namespace Syroot.Worms.Mgame.GameServer.Packets.OnlineWorms
{
/// <summary>
/// Represents the factory of packets in the OW channel packet format.
/// </summary>
internal class OWChannelPacketFormat : IPacketFormat
{
// ---- CONSTANTS ----------------------------------------------------------------------------------------------
private const ushort _tagStart = 0xFFFE;
private const ushort _tagEnd = 0xFEFF;
private const int _maxDataSize = 1024;
// ---- FIELDS -------------------------------------------------------------------------------------------------
private static readonly IList<(Type type, OWChannelPacketAttribute attrib)> _cachedClasses
= ReflectionTools.GetAttributedClasses<OWChannelPacketAttribute>();
// ---- CONSTRUCTORS & DESTRUCTOR ------------------------------------------------------------------------------
private OWChannelPacketFormat() { }
// ---- PROPERTIES ---------------------------------------------------------------------------------------------
internal static OWChannelPacketFormat Instance { get; } = new OWChannelPacketFormat();
// ---- METHODS (PUBLIC) ---------------------------------------------------------------------------------------
public IPacket TryLoad(Stream stream)
{
// Read head.
if (stream.ReadUInt16() != _tagStart || stream.Read1Byte() != 1)
return null;
int dataSize = stream.ReadUInt16();
IPacket packet = GetPacket(stream.Read1Byte());
if (packet == null)
return null;
// Read data.
SpanReader reader = new SpanReader(stream.ReadBytes(dataSize), encoding: Encodings.Korean);
// Read tail.
if (stream.ReadUInt16() != _tagEnd)
return null;
// Deserialize and return packet.
packet.Load(ref reader);
return packet;
}
public bool TrySave(Stream stream, IPacket packet)
{
byte? id = GetID(packet);
if (!id.HasValue)
return false;
// Retrieve data. Must have at least 1 byte.
SpanWriter writer = new SpanWriter(new byte[_maxDataSize], encoding: Encodings.Korean);
packet.Save(ref writer);
writer.Position = Math.Max(1, writer.Position);
// Send head.
stream.WriteUInt16(_tagStart);
stream.WriteByte(1);
stream.WriteUInt16((ushort)writer.Position);
stream.WriteByte(id.Value);
// Send data.
stream.Write(writer.Span);
// Send tail.
stream.WriteUInt16(_tagEnd);
return true;
}
// ---- METHODS (PRIVATE) --------------------------------------------------------------------------------------
private IPacket GetPacket(byte id)
{
Type packetType = _cachedClasses.Where(x => x.attrib.ID == id).FirstOrDefault().type;
return packetType == null ? null : (IPacket)Activator.CreateInstance(packetType);
}
private byte? GetID(IPacket packet)
{
Type packetType = packet.GetType();
return _cachedClasses.Where(x => x.type == packetType).FirstOrDefault().attrib?.ID;
}
}
}

View File

@ -0,0 +1,30 @@
using System;
namespace Syroot.Worms.Mgame.GameServer.Packets.OnlineWorms
{
/// <summary>
/// Decorates an <see cref="IPacket"/> child class with the ID of the OW server packet it represents.
/// </summary>
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)]
internal class OWServerPacketAttribute : Attribute
{
// ---- CONSTRUCTORS & DESTRUCTOR ------------------------------------------------------------------------------
/// <summary>
/// Initializes a new instance of the <see cref="OWServerPacketAttribute"/> class, decorating a class with the
/// ID of the packet it represents.
/// </summary>
/// <param name="id">The ID of the packet which the decorated class represents.</param>
internal OWServerPacketAttribute(ushort id)
{
ID = id;
}
// ---- PROPERTIES ---------------------------------------------------------------------------------------------
/// <summary>
/// Gets the ID of the packet the decorated class represents.
/// </summary>
internal ushort ID { get; }
}
}

View File

@ -0,0 +1,85 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Syroot.BinaryData;
using Syroot.BinaryData.Memory;
using Syroot.Worms.Mgame.GameServer.Core.Reflection;
namespace Syroot.Worms.Mgame.GameServer.Packets.OnlineWorms
{
/// <summary>
/// Represents the factory of packets in the OW server packet format.
/// </summary>
internal class OWServerPacketFormat : IPacketFormat
{
// ---- CONSTANTS ----------------------------------------------------------------------------------------------
private const int _maxDataSize = 1024;
// ---- FIELDS -------------------------------------------------------------------------------------------------
private static readonly IList<(Type type, OWServerPacketAttribute attrib)> _cachedClasses
= ReflectionTools.GetAttributedClasses<OWServerPacketAttribute>();
// ---- CONSTRUCTORS & DESTRUCTOR ------------------------------------------------------------------------------
private OWServerPacketFormat() { }
// ---- PROPERTIES ---------------------------------------------------------------------------------------------
internal static OWServerPacketFormat Instance { get; } = new OWServerPacketFormat();
// ---- METHODS (PUBLIC) ---------------------------------------------------------------------------------------
public IPacket TryLoad(Stream stream)
{
// Read head.
ushort id = stream.ReadUInt16();
IPacket packet = GetPacket(id);
if (packet == null)
return null;
int dataSize = stream.ReadUInt16();
// Read data.
SpanReader reader = new SpanReader(stream.ReadBytes(dataSize), encoding: Encodings.Korean);
// Deserialize and return packet.
packet.Load(ref reader);
return packet;
}
public bool TrySave(Stream stream, IPacket packet)
{
ushort? id = GetID(packet);
if (!id.HasValue)
return false;
// Retrieve data.
SpanWriter writer = new SpanWriter(new byte[_maxDataSize], encoding: Encodings.Korean);
packet.Save(ref writer);
// Send head.
stream.WriteUInt16(id.Value);
stream.WriteUInt16((ushort)writer.Position);
// Write data.
stream.Write(writer.Span);
return true;
}
// ---- METHODS (PRIVATE) --------------------------------------------------------------------------------------
private IPacket GetPacket(ushort id)
{
Type packetType = _cachedClasses.Where(x => x.attrib.ID == id).FirstOrDefault().type;
return packetType == null ? null : (IPacket)Activator.CreateInstance(packetType);
}
private ushort? GetID(IPacket packet)
{
Type packetType = packet.GetType();
return _cachedClasses.Where(x => x.type == packetType).FirstOrDefault().attrib?.ID;
}
}
}

View File

@ -2,12 +2,12 @@
using System.Net;
using Syroot.BinaryData.Memory;
namespace Syroot.Worms.Mgame.GameServer.Packets.OnlineWorms
namespace Syroot.Worms.Mgame.GameServer.Packets.OnlineWorms.Server
{
/// <summary>
/// Represents the client request for a <see cref="ChannelEnterReply"/>.
/// </summary>
[Packet(PacketFormat.OWServer, 0x8034)]
[OWServerPacket(0x8034)]
internal class ChannelEnterQuery : IPacket
{
// ---- PROPERTIES ---------------------------------------------------------------------------------------------

View File

@ -2,12 +2,12 @@
using System.Net;
using Syroot.BinaryData.Memory;
namespace Syroot.Worms.Mgame.GameServer.Packets.OnlineWorms
namespace Syroot.Worms.Mgame.GameServer.Packets.OnlineWorms.Server
{
/// <summary>
/// Represents the server response to a <see cref="ChannelEnterQuery"/>.
/// </summary>
[Packet(PacketFormat.OWServer, 0x8035)]
[OWServerPacket(0x8035)]
internal class ChannelEnterReply : IPacket
{
// ---- PROPERTIES ---------------------------------------------------------------------------------------------

View File

@ -5,12 +5,12 @@ using System.Net;
using Syroot.BinaryData.Memory;
using Syroot.Worms.Mgame.GameServer.Core.IO;
namespace Syroot.Worms.Mgame.GameServer.Packets.OnlineWorms
namespace Syroot.Worms.Mgame.GameServer.Packets.OnlineWorms.Server
{
/// <summary>
/// Represents an additional server response to a <see cref="LoginQuery"/>, providing available server channels.
/// </summary>
[Packet(PacketFormat.OWServer, 0x80C9)]
[OWServerPacket(0x80C9)]
internal class ChannelListReply : IPacket
{
// ---- PROPERTIES ---------------------------------------------------------------------------------------------

View File

@ -1,12 +1,12 @@
using System;
using Syroot.BinaryData.Memory;
namespace Syroot.Worms.Mgame.GameServer.Packets.OnlineWorms
namespace Syroot.Worms.Mgame.GameServer.Packets.OnlineWorms.Server
{
/// <summary>
/// Represents the client request for a <see cref="ConnectReply"/>.
/// </summary>
[Packet(PacketFormat.OWServer, 0x800E)]
[OWServerPacket(0x800E)]
internal class ConnectQuery : IPacket
{
// ---- METHODS (PUBLIC) ---------------------------------------------------------------------------------------

View File

@ -1,12 +1,12 @@
using System;
using Syroot.BinaryData.Memory;
namespace Syroot.Worms.Mgame.GameServer.Packets.OnlineWorms
namespace Syroot.Worms.Mgame.GameServer.Packets.OnlineWorms.Server
{
/// <summary>
/// Represents the server response to a <see cref="ConnectQuery"/>.
/// </summary>
[Packet(PacketFormat.OWServer, 0x800F)]
[OWServerPacket(0x800F)]
internal class ConnectReply : IPacket
{
// ---- PROPERTIES ---------------------------------------------------------------------------------------------

View File

@ -1,14 +1,14 @@
using System;
using Syroot.BinaryData.Memory;
namespace Syroot.Worms.Mgame.GameServer.Packets.OnlineWorms
namespace Syroot.Worms.Mgame.GameServer.Packets.OnlineWorms.Server
{
/// <summary>
/// Represents an additional server response to a <see cref="LoginQuery"/>, providing informational server
/// screen text.
/// </summary>
[Packet(PacketFormat.OWServer, 0x8033)]
internal class ServerInfoReply : IPacket
[OWServerPacket(0x8033)]
internal class InfoReply : IPacket
{
// ---- PROPERTIES ---------------------------------------------------------------------------------------------

View File

@ -3,12 +3,12 @@ using System.Net;
using Syroot.BinaryData.Memory;
using Syroot.Worms.Mgame.GameServer.Packets.Data;
namespace Syroot.Worms.Mgame.GameServer.Packets.OnlineWorms
namespace Syroot.Worms.Mgame.GameServer.Packets.OnlineWorms.Server
{
/// <summary>
/// Represents the client request for a <see cref="LoginReply"/>.
/// </summary>
[Packet(PacketFormat.OWServer, 0x8000)]
[OWServerPacket(0x8000)]
internal class LoginQuery : IPacket
{
// ---- PROPERTIES ---------------------------------------------------------------------------------------------

View File

@ -1,12 +1,12 @@
using System;
using Syroot.BinaryData.Memory;
namespace Syroot.Worms.Mgame.GameServer.Packets.OnlineWorms
namespace Syroot.Worms.Mgame.GameServer.Packets.OnlineWorms.Server
{
/// <summary>
/// Represents the server response to a <see cref="LoginQuery"/>.
/// </summary>
[Packet(PacketFormat.OWServer, 0x8001)]
[OWServerPacket(0x8001)]
internal class LoginReply : IPacket
{
// ---- PROPERTIES ---------------------------------------------------------------------------------------------

View File

@ -3,10 +3,10 @@
namespace Syroot.Worms.Mgame.GameServer.Packets
{
/// <summary>
/// Decorates a <see cref="IPacket"/> child class with the ID of the packet it represents.
/// Decorates an <see cref="IPacket"/> child class with the ID of the packet it represents.
/// </summary>
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = false)]
internal class PacketAttribute : Attribute
internal abstract class PacketAttribute : Attribute
{
// ---- CONSTRUCTORS & DESTRUCTOR ------------------------------------------------------------------------------
@ -15,9 +15,8 @@ namespace Syroot.Worms.Mgame.GameServer.Packets
/// packet it represents.
/// </summary>
/// <param name="id">The ID of the packet which the decorated class represents.</param>
public PacketAttribute(PacketFormat format, int id)
internal PacketAttribute(int id)
{
Format = format;
ID = id;
}
@ -26,24 +25,6 @@ namespace Syroot.Worms.Mgame.GameServer.Packets
/// <summary>
/// Gets the ID of the packet the decorated class represents.
/// </summary>
public int ID { get; }
/// <summary>
/// Gets the format in which the packet is serialized and sent over the network.
/// </summary>
public PacketFormat Format { get; }
}
/// <summary>
/// Represents the known packet formats.
/// </summary>
internal enum PacketFormat
{
/// <summary>An Online Worms server packet.</summary>
OWServer,
/// <summary>An Online Worms channel packet.</summary>
OWChannel,
/// <summary>A Worms World Party Aqua packet.</summary>
Wwpa
internal int ID { get; }
}
}

View File

@ -0,0 +1,138 @@
using System;
using System.IO;
using System.Net.Sockets;
using Syroot.Worms.Mgame.GameServer.Core.IO;
using Syroot.Worms.Mgame.GameServer.Core.Net;
using Syroot.Worms.Mgame.GameServer.Core.Reflection;
namespace Syroot.Worms.Mgame.GameServer.Packets
{
/// <summary>
/// Represents a class capable of dispatching received <see cref="IPacket"/> instances to a corresponding method.
/// </summary>
internal class PacketConnection : IDisposable
{
// ---- CONSTANTS ----------------------------------------------------------------------------------------------
private const int _maxCacheSize = 2048;
// ---- FIELDS -------------------------------------------------------------------------------------------------
private readonly MethodHandler<IPacket> _methodHandler;
private readonly SafeNetworkStream _tcpStream;
private readonly IPacketFormat[] _packetFormatPipe;
private bool _disposed;
// ---- CONSTRUCTORS & DESTRUCTOR ------------------------------------------------------------------------------
/// <summary>
/// Initializes a new instance of the <see cref="PacketConnection"/> class, handling the given
/// <paramref name="tcpClient"/>.
/// </summary>
/// <param name="tcpClient">The <see cref="System.Net.Sockets.TcpClient"/> to communicate with.</param>
/// <param name="packetFormatPipe">The array of <see cref="IPacketFormat"/> instances which are called in order
/// to handle a packet.</param>
internal PacketConnection(TcpClient tcpClient, params IPacketFormat[] packetFormatPipe)
{
TcpClient = tcpClient;
_packetFormatPipe = packetFormatPipe;
_tcpStream = TcpClient.GetSafeStream();
_methodHandler = new MethodHandler<IPacket>(this);
}
// ---- EVENTS -------------------------------------------------------------------------------------------------
protected event EventHandler<IPacket> PrePacketHandle;
protected event EventHandler<IPacket> PrePacketSend;
protected event EventHandler<Exception> UnhandledException;
// ---- PROPERTIES ---------------------------------------------------------------------------------------------
/// <summary>
/// Gets or sets the <see cref="TcpClient"/> with which the connection communicates.
/// </summary>
internal TcpClient TcpClient { get; }
// ---- METHODS (PUBLIC) ---------------------------------------------------------------------------------------
public void Dispose()
{
// Do not change this code. Put cleanup code in Dispose(bool disposing).
Dispose(true);
}
// ---- METHODS (INTERNAL) -------------------------------------------------------------------------------------
/// <summary>
/// Starts handling packets incoming from this connection and dispatches them to corresponding methods.
/// </summary>
internal void Listen()
{
try
{
IPacket packet;
while ((packet = ReceivePacket()) != null)
{
PrePacketHandle?.Invoke(this, packet);
_methodHandler.Handle(packet);
}
}
catch (Exception ex)
{
UnhandledException?.Invoke(this, ex);
}
}
// ---- METHODS (PROTECTED) ------------------------------------------------------------------------------------
protected virtual void Dispose(bool disposing)
{
if (!_disposed)
{
if (disposing)
TcpClient.Dispose();
_disposed = true;
}
}
protected void SendPacket(IPacket packet)
{
PrePacketSend?.Invoke(this, packet);
try
{
// Let each packet format try to serialize the data.
foreach (IPacketFormat format in _packetFormatPipe)
{
if (format.TrySave(_tcpStream, packet))
return;
}
throw new NotImplementedException("Cannot send unhandled packet format.");
}
catch (IOException) { } // A network error appeared, and communication should end.
catch (ObjectDisposedException) { } // The underlying stream was most apparently closed.
}
// ---- METHODS (PRIVATE) --------------------------------------------------------------------------------------
private IPacket ReceivePacket()
{
try
{
// Let each packet format read from the stream without consuming data.
using (var cachedStream = new CachedStream<SafeNetworkStream>(_tcpStream, _maxCacheSize, true))
{
foreach (IPacketFormat format in _packetFormatPipe)
{
IPacket packet = format.TryLoad(cachedStream);
if (packet != null)
return packet;
cachedStream.MoveToCacheStart();
}
}
throw new NotImplementedException("Cannot receive unhandled packet format.");
}
catch (IOException) { return null; } // The underlying socket closed or sent invalid data.
catch (ObjectDisposedException) { return null; } // The underlying stream closed.
}
}
}

View File

@ -1,75 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
namespace Syroot.Worms.Mgame.GameServer.Packets
{
/// <summary>
/// Represents a factory creating <see cref="IPacket"/> instances by mapping their ID to types to instantiate.
/// </summary>
internal static class PacketFactory
{
// ---- FIELDS -------------------------------------------------------------------------------------------------
private static readonly Dictionary<PacketAttribute, Type> _packetCache;
// ---- CONSTRUCTORS & DESTRUCTOR ------------------------------------------------------------------------------
static PacketFactory()
{
// Cache the ID-to-packet-class map.
_packetCache = new Dictionary<PacketAttribute, Type>();
foreach (Type type in Assembly.GetExecutingAssembly().GetTypes())
{
foreach (PacketAttribute attrib in type.GetCustomAttributes<PacketAttribute>())
{
if (_packetCache.ContainsKey(attrib))
{
throw new InvalidOperationException(
$"{attrib.Format} packet with ID {attrib.ID} mapped to multiple classes.");
}
_packetCache.Add(attrib, type);
}
}
}
// ---- METHODS (INTERNAL) -------------------------------------------------------------------------------------
/// <summary>
/// Creates a new <see cref="IPacket"/> instance mapped to the given <paramref name="id"/>.
/// </summary>
/// <param name="format">The <see cref="PacketFormat"/> to instantiate.</param>
/// <param name="id">The ID of the packet class to instantiate.</param>
/// <returns>The created <see cref="IPacket"/> instance.</returns>
/// <exception cref="KeyNotFoundException">No class was mapped to the given packet ID.</exception>
internal static IPacket Create(PacketFormat format, int id)
{
foreach (KeyValuePair<PacketAttribute, Type> packetMeta in _packetCache)
{
if (packetMeta.Key.ID == id)
return (IPacket)Activator.CreateInstance(packetMeta.Value, true);
}
// No packet metadata matching.
#if DEBUG
return new RawPacket(format, id);
#endif
throw new ArgumentException($"{format} packet with ID {id} could not be created.");
}
/// <summary>
/// Gets the <see cref="PacketAttribute"/> metadata for the class of the given <paramref name="packet"/>.
/// </summary>
/// <param name="packet">The <see cref="IPacket"/> whose metadata will be returned.</param>
/// <returns>The metadata of the packet class.</returns>
internal static PacketAttribute GetAttribute(IPacket packet)
{
#if DEBUG
if (packet is RawPacket rawPacket)
return new PacketAttribute(rawPacket.Type, rawPacket.ID);
else
#endif
return _packetCache.Where(x => x.Value == packet.GetType()).First().Key;
}
}
}

View File

@ -0,0 +1,29 @@
using System.IO;
namespace Syroot.Worms.Mgame.GameServer.Packets
{
/// <summary>
/// Represents the implementation of a specific packet format which can instantiate corresponding classes.
/// </summary>
internal interface IPacketFormat
{
// ---- METHODS ------------------------------------------------------------------------------------------------
/// <summary>
/// Returns an <see cref="IPacket"/> instance read from the given <paramref name="stream"/> or
/// <see langword="null"/> if the data was not compatible with this format.
/// </summary>
/// <param name="stream">The <see cref="Stream"/> to read data from.</param
/// <returns>The instantiated <see cref="IPacket"/> or <see langword="null"/>.</returns>
IPacket TryLoad(Stream stream);
/// <summary>
/// Returns a value indicating whether this format could save the <paramref name="packet"/> to the given
/// <paramref name="stream"/>.
/// </summary>
/// <param name="stream">The <see cref="Stream"/> to write data to.</param>
/// <param name="packet">The <see cref="IPacket"/> to serialize, if possibile.</param>
/// <returns><see langword="true"/> if a packet was serialized; otherwise, <see langword="false"/>.</returns>
bool TrySave(Stream stream, IPacket packet);
}
}

View File

@ -1,35 +0,0 @@
#if DEBUG
using Syroot.BinaryData.Memory;
namespace Syroot.Worms.Mgame.GameServer.Packets
{
/// <summary>
/// Represents a special fallback packet which simply stores any ID and the raw packet data.
/// </summary>
internal class RawPacket : IPacket
{
// ---- CONSTRUCTORS & DESTRUCTOR ------------------------------------------------------------------------------
internal RawPacket(PacketFormat type, int id, params byte[] data)
{
Type = type;
ID = id;
Data = data;
}
// ---- PROPERTIES ---------------------------------------------------------------------------------------------
internal PacketFormat Type { get; }
internal int ID { get; }
internal byte[] Data { get; private set; }
// ---- METHODS (INTERNAL) -------------------------------------------------------------------------------------
public void Load(ref SpanReader reader) => Data = reader.ReadBytes(reader.Length);
public void Save(ref SpanWriter writer) => writer.WriteBytes(Data);
}
}
#endif

View File

@ -6,7 +6,7 @@ namespace Syroot.Worms.Mgame.GameServer.Packets.WorldPartyAqua.Channel
/// <summary>
/// Represents the client request for a <see cref="CmdReply"/>.
/// </summary>
[Packet(PacketFormat.Wwpa, 0x10A)]
[WwpaPacket(0x10A)]
internal class CmdQuery : CmdPacket
{
// ---- FIELDS -------------------------------------------------------------------------------------------------

View File

@ -6,7 +6,7 @@ namespace Syroot.Worms.Mgame.GameServer.Packets.WorldPartyAqua.Channel
/// <summary>
/// Represents the server response to a <see cref="CmdQuery"/>.
/// </summary>
[Packet(PacketFormat.Wwpa, 0x10B)]
[WwpaPacket(0x10B)]
internal class CmdReply : CmdPacket
{
// ---- FIELDS -------------------------------------------------------------------------------------------------

View File

@ -5,7 +5,7 @@ namespace Syroot.Worms.Mgame.GameServer.Packets.WorldPartyAqua.Channel
/// <summary>
/// Represents the client notice of disconnecting.
/// </summary>
[Packet(PacketFormat.Wwpa, 0x105)]
[WwpaPacket(0x105)]
internal class DisconnectNotice : IPacket
{
// ---- METHODS (PUBLIC) ---------------------------------------------------------------------------------------

View File

@ -10,7 +10,7 @@ namespace Syroot.Worms.Mgame.GameServer.Packets.WorldPartyAqua.Channel
/// <summary>
/// Represents the client request for a <see cref="LoginReply"/>.
/// </summary>
[Packet(PacketFormat.Wwpa, 0x101)]
[WwpaPacket(0x101)]
internal class LoginQuery : IPacket
{
// ---- PROPERTIES ---------------------------------------------------------------------------------------------

View File

@ -6,7 +6,7 @@ namespace Syroot.Worms.Mgame.GameServer.Packets.WorldPartyAqua.Channel
/// <summary>
/// Represents the server response to a <see cref="LoginQuery"/>.
/// </summary>
[Packet(PacketFormat.Wwpa, 0x102)]
[WwpaPacket(0x102)]
internal class LoginReply : IPacket
{
// ---- PROPERTIES ---------------------------------------------------------------------------------------------

View File

@ -5,7 +5,7 @@ namespace Syroot.Worms.Mgame.GameServer.Packets.WorldPartyAqua.Channel
/// <summary>
/// Represents the client request for a <see cref="LogoutReply"/>.
/// </summary>
[Packet(PacketFormat.Wwpa, 0x103)]
[WwpaPacket(0x103)]
internal class LogoutQuery : IPacket
{
// ---- METHODS (PUBLIC) ---------------------------------------------------------------------------------------

View File

@ -6,7 +6,7 @@ namespace Syroot.Worms.Mgame.GameServer.Packets.WorldPartyAqua.Channel
/// <summary>
/// Represents the server response to a <see cref="LogoutQuery"/>.
/// </summary>
[Packet(PacketFormat.Wwpa, 0x104)]
[WwpaPacket(0x104)]
internal class LogoutReply : IPacket
{
// ---- PROPERTIES ---------------------------------------------------------------------------------------------

View File

@ -7,7 +7,7 @@ namespace Syroot.Worms.Mgame.GameServer.Packets.WorldPartyAqua.Channel
/// Invalid description: "Represents the server response to a <see cref="LoginQuery"/>."
/// </summary>
[Obsolete("While this packet exists, it is currently unclear where it is used.")]
[Packet(PacketFormat.Wwpa, 0x145)]
[WwpaPacket(0x145)]
internal class NotLoginReply : IPacket
{
// ---- METHODS (PUBLIC) ---------------------------------------------------------------------------------------

View File

@ -5,7 +5,7 @@ namespace Syroot.Worms.Mgame.GameServer.Packets.WorldPartyAqua.Channel
/// <summary>
/// Represents the client request for a <see cref="Unknown11FReply"/>.
/// </summary>
[Packet(PacketFormat.Wwpa, 0x11E)]
[WwpaPacket(0x11E)]
internal class Unknown11EQuery : IPacket
{
// ---- METHODS (PUBLIC) ---------------------------------------------------------------------------------------

View File

@ -6,7 +6,7 @@ namespace Syroot.Worms.Mgame.GameServer.Packets.WorldPartyAqua.Channel
/// <summary>
/// Represents the server response to a <see cref="Unknown11EQuery"/>.
/// </summary>
[Packet(PacketFormat.Wwpa, 0x11F)]
[WwpaPacket(0x11F)]
internal class Unknown11FReply : IPacket
{
// ---- PROPERTIES ---------------------------------------------------------------------------------------------

View File

@ -5,7 +5,7 @@ namespace Syroot.Worms.Mgame.GameServer.Packets.WorldPartyAqua.Channel
/// <summary>
/// Represents the client request for a <see cref="Unknown15BReply"/>.
/// </summary>
[Packet(PacketFormat.Wwpa, 0x15A)]
[WwpaPacket(0x15A)]
internal class Unknown15AQuery : IPacket
{
// ---- METHODS (PUBLIC) ---------------------------------------------------------------------------------------

View File

@ -6,7 +6,7 @@ namespace Syroot.Worms.Mgame.GameServer.Packets.WorldPartyAqua.Channel
/// <summary>
/// Represents the server response to a <see cref="Unknown15AQuery"/>.
/// </summary>
[Packet(PacketFormat.Wwpa, 0x15B)]
[WwpaPacket(0x15B)]
internal class Unknown15BReply : IPacket
{
// ---- PROPERTIES ---------------------------------------------------------------------------------------------

View File

@ -0,0 +1,29 @@
using Syroot.BinaryData.Memory;
using Syroot.Worms.Mgame.GameServer.Core.IO;
namespace Syroot.Worms.Mgame.GameServer.Packets.WorldPartyAqua
{
/// <summary>
/// Represents a fallback WWPA packet.
/// </summary>
public class RawPacket : IPacket
{
// ---- PROPERTIES ---------------------------------------------------------------------------------------------
public int ID { get; set; }
public byte[] Data { get; set; }
// ---- METHODS (PUBLIC) ---------------------------------------------------------------------------------------
public void Load(ref SpanReader reader)
{
Data = reader.ReadToEnd();
}
public void Save(ref SpanWriter writer)
{
writer.WriteBytes(Data);
}
}
}

View File

@ -5,7 +5,7 @@ namespace Syroot.Worms.Mgame.GameServer.Packets.WorldPartyAqua.Server
/// <summary>
/// Represents the client request for a <see cref="ChannelListReply"/>.
/// </summary>
[Packet(PacketFormat.Wwpa, 0x801E)]
[WwpaPacket(0x801E)]
internal class ChannelListQuery : IPacket
{
// ---- METHODS (PUBLIC) ---------------------------------------------------------------------------------------

View File

@ -10,7 +10,7 @@ namespace Syroot.Worms.Mgame.GameServer.Packets.WorldPartyAqua.Server
/// <summary>
/// Represents the server reply to a <see cref="ChannelListQuery"/>.
/// </summary>
[Packet(PacketFormat.Wwpa, 0x801F)]
[WwpaPacket(0x801F)]
internal class ChannelListReply : IPacket
{
// ---- PROPERTIES ---------------------------------------------------------------------------------------------

View File

@ -5,7 +5,7 @@ namespace Syroot.Worms.Mgame.GameServer.Packets.WorldPartyAqua.Server
/// <summary>
/// Represents the client request for a <see cref="ConnectReply"/>.
/// </summary>
[Packet(PacketFormat.Wwpa, 0x8001)]
[WwpaPacket(0x8001)]
internal class ConnectQuery : IPacket
{
// ---- METHODS (PUBLIC) ---------------------------------------------------------------------------------------

View File

@ -6,7 +6,7 @@ namespace Syroot.Worms.Mgame.GameServer.Packets.WorldPartyAqua.Server
/// <summary>
/// Represents the server response to a <see cref="ConnectQuery"/>.
/// </summary>
[Packet(PacketFormat.Wwpa, 0x8002)]
[WwpaPacket(0x8002)]
internal class ConnectReply : IPacket
{
// ---- PROPERTIES ---------------------------------------------------------------------------------------------

View File

@ -5,7 +5,7 @@ namespace Syroot.Worms.Mgame.GameServer.Packets.WorldPartyAqua.Server
/// <summary>
/// Represents the client notice of disconnecting.
/// </summary>
[Packet(PacketFormat.Wwpa, 0x801B)]
[WwpaPacket(0x801B)]
internal class DisconnectNotice : IPacket
{
// ---- METHODS (PUBLIC) ---------------------------------------------------------------------------------------

View File

@ -8,7 +8,7 @@ namespace Syroot.Worms.Mgame.GameServer.Packets.WorldPartyAqua.Server
/// <summary>
/// Represents the client request for a <see cref="LoginReply"/>.
/// </summary>
[Packet(PacketFormat.Wwpa, 0x8019)]
[WwpaPacket(0x8019)]
internal class LoginQuery : IPacket
{
// ---- PROPERTIES ---------------------------------------------------------------------------------------------

View File

@ -6,7 +6,7 @@ namespace Syroot.Worms.Mgame.GameServer.Packets.WorldPartyAqua.Server
/// <summary>
/// Represents the server response to a <see cref="LoginQuery"/>.
/// </summary>
[Packet(PacketFormat.Wwpa, 0x801A)]
[WwpaPacket(0x801A)]
internal class LoginReply : IPacket
{
// ---- PROPERTIES ---------------------------------------------------------------------------------------------

View File

@ -6,7 +6,7 @@ namespace Syroot.Worms.Mgame.GameServer.Packets.WorldPartyAqua.Server
/// <summary>
/// Represents the client request for an <see cref="OptionsReply"/>.
/// </summary>
[Packet(PacketFormat.Wwpa, 0x8046)]
[WwpaPacket(0x8046)]
internal class OptionsQuery : IPacket
{
// ---- PROPERTIES ---------------------------------------------------------------------------------------------

View File

@ -7,7 +7,7 @@ namespace Syroot.Worms.Mgame.GameServer.Packets.WorldPartyAqua.Server
/// <summary>
/// Represents the server response to a <see cref="OptionsQuery"/>.
/// </summary>
[Packet(PacketFormat.Wwpa, 0x8047)]
[WwpaPacket(0x8047)]
internal class OptionsReply : IPacket
{
// ---- PROPERTIES ---------------------------------------------------------------------------------------------

View File

@ -7,7 +7,7 @@ namespace Syroot.Worms.Mgame.GameServer.Packets.WorldPartyAqua.Server
/// <summary>
/// Represents the client request for an <see cref="OptionsSaveReply"/>.
/// </summary>
[Packet(PacketFormat.Wwpa, 0x8048)]
[WwpaPacket(0x8048)]
internal class OptionsSaveQuery : IPacket
{
// ---- PROPERTIES ---------------------------------------------------------------------------------------------

View File

@ -6,7 +6,7 @@ namespace Syroot.Worms.Mgame.GameServer.Packets.WorldPartyAqua.Server
/// <summary>
/// Represents the server response to an <see cref="OptionsSaveQuery"/>.
/// </summary>
[Packet(PacketFormat.Wwpa, 0x8049)]
[WwpaPacket(0x8049)]
internal class OptionsSaveReply : IPacket
{
// ---- PROPERTIES ---------------------------------------------------------------------------------------------

View File

@ -0,0 +1,35 @@
using System;
namespace Syroot.Worms.Mgame.GameServer.Packets.WorldPartyAqua
{
/// <summary>
/// Decorates an <see cref="IPacket"/> child class with the ID of the OW server packet it represents.
/// </summary>
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)]
internal class WwpaPacketAttribute : Attribute
{
// ---- CONSTRUCTORS & DESTRUCTOR ------------------------------------------------------------------------------
/// <summary>
/// Initializes a new instance of the <see cref="WwpaPacketAttribute"/> class, decorating a class with the ID of
/// the packet it represents.
/// </summary>
/// <param name="id">The ID of the packet which the decorated class represents.</param>
internal WwpaPacketAttribute(int id)
{
ID = id;
}
// ---- PROPERTIES ---------------------------------------------------------------------------------------------
/// <summary>
/// Gets the ID of the packet the decorated class represents.
/// </summary>
internal int ID { get; }
/// <summary>
/// Gets or sets an optional command number which the packet represents.
/// </summary>
internal int Command { get; set; }
}
}

View File

@ -0,0 +1,124 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Syroot.BinaryData;
using Syroot.BinaryData.Memory;
using Syroot.Worms.Mgame.GameServer.Core.Reflection;
namespace Syroot.Worms.Mgame.GameServer.Packets.WorldPartyAqua
{
/// <summary>
/// Represents the implementation of a specific packet format which can instantiate corresponding classes.
/// </summary>
internal class WwpaPacketFormat : IPacketFormat
{
// ---- CONSTANTS ----------------------------------------------------------------------------------------------
private const ushort _tagStart = 0x2E9E;
private const ushort _tagEnd = 0x7F3F;
private const int _maxDataSize = 1024;
// ---- FIELDS -------------------------------------------------------------------------------------------------
private static readonly IList<(Type type, WwpaPacketAttribute attrib)> _cachedClasses
= ReflectionTools.GetAttributedClasses<WwpaPacketAttribute>();
// ---- CONSTRUCTORS & DESTRUCTOR ------------------------------------------------------------------------------
private WwpaPacketFormat() { }
// ---- PROPERTIES ---------------------------------------------------------------------------------------------
internal static WwpaPacketFormat Instance { get; } = new WwpaPacketFormat();
// ---- METHODS (PUBLIC) ---------------------------------------------------------------------------------------
public IPacket TryLoad(Stream stream)
{
// Read head.
if (stream.ReadUInt16() != _tagStart
|| !stream.ReadBoolean() // bHead2, always true
|| stream.ReadBoolean() // bHead3, always false
|| !stream.ReadBoolean()) // bCompressed, always true
return null;
int decompressedSize = stream.ReadUInt16();
int compressedSize = stream.ReadInt32();
int idxPacket = stream.ReadInt32();
// Read data.
byte[] compressedData = stream.ReadBytes(compressedSize);
// Read tail.
if (stream.ReadInt32() != idxPacket || stream.ReadUInt16() != _tagEnd)
return null;
// Instantiate, deserialize, and return packet.
SpanReader reader = new SpanReader(PacketCompression.Decompress(compressedData), encoding: Encodings.Korean);
int id = reader.ReadInt32();
IPacket packet = GetPacket(id);
if (packet == null)
return null;
SpanReader dataReader = reader.Slice();
packet.Load(ref dataReader);
return packet;
}
public bool TrySave(Stream stream, IPacket packet)
{
int? id = GetID(packet);
if (!id.HasValue)
return false;
// Retrieve (decompressed) data.
SpanWriter writer = new SpanWriter(new byte[_maxDataSize], encoding: Encodings.Korean);
writer.WriteInt32(id.Value);
SpanWriter dataWriter = writer.Slice();
packet.Save(ref dataWriter);
writer.Position += dataWriter.Position;
// Send head and data.
stream.WriteUInt16(_tagStart);
stream.WriteBoolean(true); // bHead2 (unknown)
stream.WriteBoolean(false); // bHead3 (unknown)
bool compress = false; // TODO: Does not get accepted by client if compressed.
stream.WriteBoolean(compress);
if (compress)
{
ReadOnlySpan<byte> compressedData = PacketCompression.Compress(writer.Span);
stream.WriteUInt16((ushort)writer.Position); // decompressed size
stream.WriteUInt32((uint)compressedData.Length); // compressed size
stream.WriteInt32(1); // index
stream.Write(compressedData);
}
else
{
stream.WriteUInt16(0); // unused
stream.WriteUInt32((uint)writer.Position); // raw size
stream.WriteInt32(1); // index
stream.Write(writer.Span.Slice(0, writer.Position));
}
// Send tail.
stream.WriteInt32(1); // index (must equal other)
stream.WriteUInt16(_tagEnd);
return true;
}
// ---- METHODS (PRIVATE) --------------------------------------------------------------------------------------
private IPacket GetPacket(int id)
{
Type packetType = _cachedClasses.Where(x => x.attrib.ID == id).FirstOrDefault().type;
return packetType == null
? new RawPacket { ID = id }
: (IPacket)Activator.CreateInstance(packetType);
}
private int? GetID(IPacket packet)
{
Type packetType = packet.GetType();
return _cachedClasses.Where(x => x.type == packetType).FirstOrDefault().attrib?.ID;
}
}
}