Implement room chat and leaves.

This commit is contained in:
Ray Koopa 2020-07-10 18:45:10 +02:00
parent 9aad0faf6b
commit 0fee2e1e13
7 changed files with 259 additions and 68 deletions

View File

@ -3,7 +3,6 @@ using System.IO;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;
namespace Syroot.Worms.IO
{
@ -19,29 +18,16 @@ namespace Syroot.Worms.IO
/// </summary>
/// <param name="stream">The <see cref="Stream"/> instance to read with.</param>
/// <param name="length">The number of bytes the fixed-size blocks takes.</param>
/// <param name="encoding">The 1-byte <see cref="Encoding"/> to use or <see langword="null"/> to use
/// <see cref="Encoding.ASCII"/>.</param>
/// <returns>The read string.</returns>
public static unsafe string ReadFixedString(this Stream stream, int length)
public static unsafe string ReadFixedString(this Stream stream, int length, Encoding? encoding = null)
{
// Ensure to not try to decode any bytes after the 0 termination.
Span<byte> bytes = stackalloc byte[length];
stream.Read(bytes);
for (length = 0; length < bytes.Length && bytes[length] != 0; length++) ;
return Encoding.ASCII.GetString(bytes.Slice(0, length));
}
/// <summary>
/// Reads a 0-terminated string which is stored in a fixed-size block of <paramref name="length"/> bytes.
/// </summary>
/// <param name="stream">The <see cref="Stream"/> instance to read with.</param>
/// <param name="length">The number of bytes the fixed-size blocks takes.</param>
/// <returns>The read string.</returns>
public static async Task<string> ReadFixedStringAsync(this Stream stream, int length)
{
// Ensure to not try to decode any bytes after the 0 termination.
byte[] bytes = new byte[length];
await stream.ReadAsync(bytes, 0, length);
for (length = 0; length < bytes.Length && bytes[length] != 0; length++) ;
return Encoding.ASCII.GetString(bytes, 0, length);
return (encoding ?? Encoding.ASCII).GetString(bytes.Slice(0, length));
}
/// <s
@ -73,28 +59,16 @@ namespace Syroot.Worms.IO
/// <param name="stream">The <see cref="Stream"/> instance to write with.</param>
/// <param name="value">The string to write.</param>
/// <param name="length">The number of bytes the fixed-size block takes.</param>
public static void WriteFixedString(this Stream stream, string value, int length)
/// <param name="encoding">The 1-byte <see cref="Encoding"/> to use or <see langword="null"/> to use
/// <see cref="Encoding.ASCII"/>.</param>
public static void WriteFixedString(this Stream stream, string value, int length, Encoding? encoding = null)
{
Span<byte> bytes = stackalloc byte[length];
if (value != null)
Encoding.ASCII.GetBytes(value.AsSpan(), bytes);
(encoding ?? Encoding.ASCII).GetBytes(value.AsSpan(), bytes);
stream.Write(bytes);
}
/// <summary>
/// Writes the given string into a fixed-size block of <paramref name="length"/> bytes and 0-terminates it.
/// </summary>
/// <param name="stream">The <see cref="Stream"/> instance to write with.</param>
/// <param name="value">The string to write.</param>
/// <param name="length">The number of bytes the fixed-size block takes.</param>
public static async Task WriteFixedStringAsync(this Stream stream, string value, int length)
{
byte[] bytes = new byte[length];
if (value != null)
Encoding.ASCII.GetBytes(value.AsSpan(), bytes);
await stream.WriteAsync(bytes, 0, length);
}
/// <summary>
/// Writes the unmanaged representation of a struct of type <typeparamref name="T"/>.
/// </summary>

View File

@ -0,0 +1,26 @@
using System.Text;
namespace Syroot.Worms.Worms2.GameServer
{
/// <summary>
/// Represents additional code pages.
/// </summary>
internal static class Encodings
{
// ---- FIELDS -------------------------------------------------------------------------------------------------
/// <summary>Windows-1252 encoding (codepage 1252).</summary>
internal static readonly Encoding Windows1252;
// ---- CONSTRUCTORS & DESTRUCTOR ------------------------------------------------------------------------------
/// <summary>
/// Initializes static members of the <see cref="Encodings"/> class.
/// </summary>
static Encodings()
{
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
Windows1252 = Encoding.GetEncoding(1252);
}
}
}

View File

@ -6,6 +6,7 @@
/// <remarks>Names are in ISO 3166 Alpha-2 notation.</remarks>
public enum Nation : byte
{
/// <summary>No flag.</summary>
None,
/// <summary>United Kingdom</summary>
UK,

View File

@ -6,12 +6,32 @@ using Syroot.Worms.IO;
namespace Syroot.Worms.Worms2.GameServer
{
/// <summary>
/// Represents a unit of communication between Worms 2 client and server.
/// </summary>
internal class Packet
{
// ---- CONSTRUCTORS & DESTRUCTOR ------------------------------------------------------------------------------
/// <summary>
/// Initializes a new instance of the <see cref="Packet"/> class.
/// </summary>
internal Packet() { }
/// <summary>
/// Initializes a new instance of the <see cref="Packet"/> class with the given contents.
/// </summary>
/// <param name="code">The <see cref="PacketCode"/> describing the action of the packet.</param>
/// <param name="value0">A parameter for the action.</param>
/// <param name="value1">A parameter for the action.</param>
/// <param name="value2">A parameter for the action.</param>
/// <param name="value3">A parameter for the action.</param>
/// <param name="value4">A parameter for the action.</param>
/// <param name="value10">A parameter for the action.</param>
/// <param name="data">A textual parameter for the action.</param>
/// <param name="error">An error code returned from the server after executing the action.</param>
/// <param name="name">A named parameter for the action.</param>
/// <param name="session">A <see cref="SessionInfo"/> for the action.</param>
internal Packet(PacketCode code,
int? value0 = null, int? value1 = null, int? value2 = null, int? value3 = null, int? value4 = null,
int? value10 = null, string? data = null, int? error = null,
@ -32,16 +52,59 @@ namespace Syroot.Worms.Worms2.GameServer
// ---- PROPERTIES ---------------------------------------------------------------------------------------------
/// <summary>
/// Gets or sets <see cref="PacketCode"/> describing the action of the packet.
/// </summary>
internal PacketCode Code { get; set; }
/// <summary>
/// Gets or sets a parameter for the action.
/// </summary>
internal int? Value0 { get; set; }
/// <summary>
/// Gets or sets a parameter for the action.
/// </summary>
internal int? Value1 { get; set; }
/// <summary>
/// Gets or sets a parameter for the action.
/// </summary>
internal int? Value2 { get; set; }
/// <summary>
/// Gets or sets a parameter for the action.
/// </summary>
internal int? Value3 { get; set; }
/// <summary>
/// Gets or sets a parameter for the action.
/// </summary>
internal int? Value4 { get; set; }
/// <summary>
/// Gets or sets a parameter for the action.
/// </summary>
internal int? Value10 { get; set; }
/// <summary>
/// Gets or sets a textual parameter for the action.
/// </summary>
internal string? Data { get; set; }
/// <summary>
/// Gets or sets an error code returned from the server after executing the action.
/// </summary>
internal int? Error { get; set; }
/// <summary>
/// Gets or sets a named parameter for the action.
/// </summary>
internal string? Name { get; set; }
/// <summary>
/// Gets or sets a <see cref="SessionInfo"/> for the action.
/// </summary>
internal SessionInfo? Session { get; set; }
// ---- METHODS (PUBLIC) ---------------------------------------------------------------------------------------
@ -68,6 +131,10 @@ namespace Syroot.Worms.Worms2.GameServer
// ---- METHODS (INTERNAL) -------------------------------------------------------------------------------------
/// <summary>
/// Blocks and reads the packet data from the given <paramref name="stream"/>.
/// </summary>
/// <param name="stream">The <see cref="Stream"/> to read the packet data from.</param>
internal void Receive(Stream stream)
{
int dataLength = 0;
@ -80,12 +147,16 @@ namespace Syroot.Worms.Worms2.GameServer
if (flags.HasFlag(Flags.HasValue4)) Value4 = stream.ReadInt32();
if (flags.HasFlag(Flags.HasValue10)) Value10 = stream.ReadInt32();
if (flags.HasFlag(Flags.HasDataLength)) dataLength = stream.ReadInt32();
if (flags.HasFlag(Flags.HasData) && dataLength != 0) Data = stream.ReadFixedString(dataLength);
if (flags.HasFlag(Flags.HasData) && dataLength != 0) Data = stream.ReadFixedString(dataLength, Encodings.Windows1252);
if (flags.HasFlag(Flags.HasError)) Error = stream.ReadInt32();
if (flags.HasFlag(Flags.HasUserName)) Name = stream.ReadFixedString(20);
if (flags.HasFlag(Flags.HasUserInfo)) Session = stream.ReadStruct<SessionInfo>();
if (flags.HasFlag(Flags.HasName)) Name = stream.ReadFixedString(20, Encodings.Windows1252);
if (flags.HasFlag(Flags.HasSession)) Session = stream.ReadStruct<SessionInfo>();
}
/// <summary>
/// Blocks and writes the packet data to the given <paramref name="stream"/>.
/// </summary>
/// <param name="stream">The <see cref="Stream"/> to write the packet data to.</param>
internal void Send(Stream stream)
{
stream.WriteInt32((int)Code);
@ -98,11 +169,11 @@ namespace Syroot.Worms.Worms2.GameServer
if (Value10.HasValue) stream.WriteInt32(Value10.Value);
if (Data != null)
{
stream.WriteInt32(Data.Length);
stream.WriteFixedString(Data, Data.Length);
stream.WriteInt32(Data.Length + 1);
stream.WriteFixedString(Data, Data.Length + 1, Encodings.Windows1252);
}
if (Error.HasValue) stream.WriteInt32(Error.Value);
if (Name != null) stream.WriteFixedString(Name, 20);
if (Name != null) stream.WriteFixedString(Name, 20, Encodings.Windows1252);
if (Session.HasValue) stream.WriteStruct(Session.Value);
}
@ -123,8 +194,8 @@ namespace Syroot.Worms.Worms2.GameServer
flags |= Flags.HasData;
}
if (Error.HasValue) flags |= Flags.HasError;
if (Name != null) flags |= Flags.HasUserName;
if (Session.HasValue) flags |= Flags.HasUserInfo;
if (Name != null) flags |= Flags.HasName;
if (Session.HasValue) flags |= Flags.HasSession;
return flags;
}
@ -143,8 +214,8 @@ namespace Syroot.Worms.Worms2.GameServer
HasDataLength = 1 << 5,
HasData = 1 << 6,
HasError = 1 << 7,
HasUserName = 1 << 8,
HasUserInfo = 1 << 9
HasName = 1 << 8,
HasSession = 1 << 9
}
}
}

View File

@ -1,4 +1,6 @@
namespace Syroot.Worms.Worms2.GameServer
using System.Net;
namespace Syroot.Worms.Worms2.GameServer
{
/// <summary>
/// Represents a room in which users can meet, chat, and host games.
@ -13,11 +15,13 @@
/// <param name="id">The unique numerical identifier of the room.</param>
/// <param name="name">The name of the room as given by the creator.</param>
/// <param name="nation">The flag displayed with the room.</param>
internal Room(int id, string name, Nation nation)
/// <param name="ipAddress">The IP address of the creator of the room.</param>
internal Room(int id, string name, Nation nation, IPAddress ipAddress)
{
ID = id;
Name = name;
Session = new SessionInfo(nation, SessionType.Room);
IPAddress = ipAddress;
}
// ---- PROPERTIES ---------------------------------------------------------------------------------------------
@ -36,5 +40,10 @@
/// Gets the <see cref="SessionInfo"/> describing the room.
/// </summary>
internal SessionInfo Session { get; }
/// <summary>
/// Gets the IP address of the creator of the room.
/// </summary>
internal IPAddress IPAddress { get; set; }
}
}

View File

@ -5,7 +5,6 @@ using System.Drawing;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Threading;
using System.Threading.Tasks;
using Syroot.ColoredConsole;
@ -18,7 +17,7 @@ namespace Syroot.Worms.Worms2.GameServer
{
// ---- FIELDS -------------------------------------------------------------------------------------------------
private int _lastID;
private int _lastID = 0x1000; // start at an offset to prevent bugs with chat
private readonly List<User> _users = new List<User>();
private readonly List<Room> _rooms = new List<Room>();
private readonly List<Game> _games = new List<Game>();
@ -39,9 +38,10 @@ namespace Syroot.Worms.Worms2.GameServer
[PacketCode.CreateRoom] = OnCreateRoom,
[PacketCode.Join] = OnJoinRoom,
[PacketCode.LeaveRoom] = OnLeaveRoom,
[PacketCode.CloseRoom] = OnCloseRoom,
[PacketCode.CreateGame] = OnCreateGame,
[PacketCode.ChatRoom] = OnChatRoom,
[PacketCode.ConnectGame] = OnConnectGame
[PacketCode.ConnectGame] = OnConnectGame,
};
// ---- METHODS (INTERNAL) -------------------------------------------------------------------------------------
@ -74,8 +74,6 @@ namespace Syroot.Worms.Worms2.GameServer
ColorConsole.WriteLine(Color.Magenta, $"{connection.RemoteEndPoint} << {packet}");
}
private int GetNextID() => Interlocked.Increment(ref _lastID);
private User? GetUser(PacketConnection connection)
{
foreach (User user in _users)
@ -185,7 +183,7 @@ namespace Syroot.Worms.Worms2.GameServer
&& packet.Session.HasValue
&& !_users.Any(x => x.Name.Equals(packet.Name, StringComparison.InvariantCultureIgnoreCase)))
{
User newUser = new User(connection, GetNextID(), packet.Name, packet.Session.Value.Nation);
User newUser = new User(connection, ++_lastID, packet.Name, packet.Session.Value.Nation);
// Notify other users about new user.
foreach (User user in _users)
@ -206,6 +204,7 @@ namespace Syroot.Worms.Worms2.GameServer
else
{
SendPacket(connection, new Packet(PacketCode.LoginReply,
value1: 0,
error: 1));
}
}
@ -221,23 +220,25 @@ namespace Syroot.Worms.Worms2.GameServer
&& packet.Session.HasValue
&& !_rooms.Any(x => x.Name.Equals(packet.Name, StringComparison.InvariantCultureIgnoreCase)))
{
Room newRoom = new Room(GetNextID(), packet.Name, packet.Session.Value.Nation);
Room newRoom = new Room(++_lastID, packet.Name, packet.Session.Value.Nation,
connection.RemoteEndPoint.Address);
_rooms.Add(newRoom);
// Send reply to creator.
SendPacket(connection, new Packet(PacketCode.CreateRoomReply,
value1: newRoom.ID,
error: 0));
// Notify other users about new room.
foreach (User user in _users.Where(x => x != fromUser))
{
SendPacket(user.Connection, new Packet(PacketCode.CreateRoom,
value1: newRoom.ID,
value4: 0,
data: String.Empty, // do not report creator IP
name: newRoom.Name,
session: newRoom.Session));
}
// Send reply to creator.
SendPacket(connection, new Packet(PacketCode.CreateRoomReply,
value1: newRoom.ID,
error: 0));
}
else
{
@ -261,7 +262,7 @@ namespace Syroot.Worms.Worms2.GameServer
foreach (User user in _users.Where(x => x != fromUser))
{
SendPacket(user.Connection, new Packet(PacketCode.Join,
value1: fromUser.RoomID,
value2: fromUser.RoomID,
value10: fromUser.ID));
}
@ -278,26 +279,84 @@ namespace Syroot.Worms.Worms2.GameServer
private void OnLeaveRoom(PacketConnection connection, Packet packet)
{
SendPacket(connection, new Packet(PacketCode.LeaveRoomReply,
error: 0));
User? fromUser = GetUser(connection);
if (fromUser == null)
return;
// Require left room ID and user ID.
if (packet.Value2.HasValue && packet.Value2 == fromUser.RoomID
&& packet.Value10.HasValue && packet.Value10 == fromUser.ID)
{
LeaveRoom(fromUser);
// Reply to leaver.
SendPacket(connection, new Packet(PacketCode.LeaveRoomReply,
error: 0));
}
else
{
// Reply to leaver.
SendPacket(connection, new Packet(PacketCode.LeaveRoomReply,
error: 1));
}
}
private void LeaveRoom(User fromUser)
{
// Close an abandoned room.
bool roomClosed = fromUser.RoomID != 0 && !_users.Any(x => x != fromUser && x.RoomID == fromUser.RoomID);
if (roomClosed)
_rooms.Remove(_rooms.Single(x => x.ID == fromUser.RoomID));
// Notify other users.
foreach (User user in _users.Where(x => x != fromUser))
{
// Notify room leave, if any.
if (fromUser.RoomID != 0)
{
SendPacket(user.Connection, new Packet(PacketCode.LeaveRoom,
value2: fromUser.RoomID,
value10: fromUser.ID));
}
// Notify room close, if any.
if (roomClosed)
{
SendPacket(user.Connection, new Packet(PacketCode.CloseRoom,
value10: fromUser.RoomID));
}
}
// Update user room.
fromUser.RoomID = 0;
}
private void OnDisconnectUser(PacketConnection connection)
{
User? disconnectedUser = GetUser(connection);
if (disconnectedUser == null)
User? fromUser = GetUser(connection);
if (fromUser == null)
return;
_users.Remove(disconnectedUser);
_users.Remove(fromUser);
// Notify other users.
LeaveRoom(fromUser);
// Notify user disconnect.
foreach (User user in _users)
{
SendPacket(user.Connection, new Packet(PacketCode.DisconnectUser,
value10: disconnectedUser.ID));
value10: fromUser.ID));
}
}
private void OnCloseRoom(PacketConnection connection, Packet packet)
{
User? fromUser = GetUser(connection);
if (fromUser == null)
return;
// Simply reply success to client, the server decides when to actually close rooms.
SendPacket(connection, new Packet(PacketCode.CloseRoomReply, error: 0));
}
private void OnCreateGame(PacketConnection connection, Packet packet)
{
SendPacket(connection, new Packet(PacketCode.CreateGameReply,
@ -307,8 +366,58 @@ namespace Syroot.Worms.Worms2.GameServer
private void OnChatRoom(PacketConnection connection, Packet packet)
{
SendPacket(connection, new Packet(PacketCode.ChatRoomReply,
error: 0));
User? fromUser = GetUser(connection);
if (fromUser == null)
return;
// Requires user ID, target ID, and message data.
if (!packet.Value0.HasValue || !packet.Value3.HasValue || packet.Data == null)
return;
int targetID = packet.Value3.Value;
string prefix;
if (packet.Data.StartsWith(prefix = $"GRP:[ {fromUser.Name} ] ", StringComparison.InvariantCulture))
{
// Check if user can access the room.
if (fromUser.RoomID == targetID)
{
// Notify all users of the room.
string message = packet.Data.Substring(prefix.Length);
foreach (User user in _users.Where(x => x.RoomID == fromUser.RoomID && x != fromUser))
{
SendPacket(user.Connection, new Packet(PacketCode.ChatRoom,
value0: fromUser.ID,
value3: user.RoomID,
data: prefix + message));
}
// Notify sender.
SendPacket(connection, new Packet(PacketCode.ChatRoomReply, error: 0));
}
else
{
SendPacket(connection, new Packet(PacketCode.ChatRoomReply, error: 1));
}
}
else if (packet.Data.StartsWith(prefix = $"PRV:[ {fromUser.Name} ] ", StringComparison.InvariantCulture))
{
// Check if user can access the user.
User? user = _users.FirstOrDefault(x => x.RoomID == fromUser.RoomID && x.ID == targetID);
if (user == null)
{
SendPacket(connection, new Packet(PacketCode.ChatRoomReply, error: 1));
}
else
{
// Notify receiver of the message.
string message = packet.Data.Substring(prefix.Length);
SendPacket(user.Connection, new Packet(PacketCode.ChatRoom,
value0: fromUser.ID,
value3: user.ID,
data: prefix + message));
// Notify sender.
SendPacket(connection, new Packet(PacketCode.ChatRoomReply, error: 0));
}
}
}
private void OnConnectGame(PacketConnection connection, Packet packet)

View File

@ -8,6 +8,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Syroot.ColoredConsole" Version="1.0.0" />
<PackageReference Include="System.Text.Encoding.CodePages" Version="4.7.1" />
<ProjectReference Include="..\..\library\Syroot.Worms\Syroot.Worms.csproj" />
</ItemGroup>
</Project>