1.7.6 • Published 2 years ago

etwin-socket-server v1.7.6

Weekly downloads
2
License
MIT
Repository
gitlab
Last release
2 years ago

Etwin Server Socket Library Showcase

This library forces you to be focused on the game development, socket are nearly managed in a automated way, so you don't have to write "socket" word a single time

It follows the SOLID philosophy, so you'll have to extend classes inside the lib (2 abstract classes and 2 interface)

Here is a simple way to make an anonymous chat:

import { APlayer, ARealtimeGame, ISendPacket, IReceivedPacket } from "etwin-socket-server";

export class ChatUser extends APlayer
{
	public readonly username: string;
	
	constructor(opts?: any)
	{
		super();
		this.username = opts?.username || "guest";
	}
}

export class MyWonderfulChat extends ARealtimeGame<ChatUser>
{
	private static readonly MAX_SIMULTANEOUS_CONNECTIONS = 10;
	
	constructor(opts?: any)
	{
		super(opts);
		this.on("receive_message", (packet: MessagePacket, emitter: ChatUser) =>
		{
			this.broadcast<MessagePacket>("send_message", {
				message: MyWonderfulChat.format(emitter.username, packet.message)
			});
		});
	}
}

interface MessagePacket extends IReceivedPacket, ISendPacket
{
	message: string;
}

You must extend APlayer & ARealtimeGame<P extends APlayer> classes, the first to represent your player data, and the other to represent the game in itself (In more long program, it is suggested to only have a file for each).

You can listen to an event with the this.on function, broadcast a message with this.broadcast, or send a message to a specific user or group of users, but we'll see it below.

import { MyWonderfulChat, ChatUser } from "./MyWonderfulChat";

function main()
{
	const ss = new SocketServer(MyWonderfulChat, ChatUser);

	ss.createServer(SocketServer.Type.IO, 3000);
	ss.createServer(SocketServer.Type.TCP, 3001);
	ss.run();
}

You'll have to instanciate a SocketServer, and pass your game and your player constructors as parameters. The SocketServer creates the type of socket server you asked for, to the port you binded with createServer function.

If you choose to bind multiple ports to multiple server-types, sockets of the SocketServer can communicate accross protocols and ports (e.g. a TCP connexion on port 3001 can communicate with an IO connexion on port 3000).

It also instanciates Rooms & Players for you (It is the reason why you have to pass their constructor), so you haven't to manage this part of the code. As explained before, with this lib you can focus on the game-logic without thinking about anything else.

Connect a socket to the server

You must always send JSON objects to the server. But, if you are using WS or TCP, you must stringify them before being able to use them.

Connect a client to a server

On connecting, you can define a range of parameters that can be used by the server. The event to send is room. The packet can contain following data:

  • opts: any => These data can be found inside the game's constructor throught the opts parameter.
  • opts.public: boolean (default: true) => If set to true, if a room can be find, it will join it. Otherwise it will create a new public room that anyone can join. If set to false, it will create a private room.
  • opts.roomCapacity: number (default: 4) => The maximum amount of connexions to the room.
  • opts.id: string => When creating a private room, an ID will be sent to the client socket. If another player wants to join, it must obtain this ID and use it in this option.
  • playerOpts: any => Specific parameters to the client. They will be obtained as parameter of the Player's class constructor.

Example:

{
	"playerOpts": { "username": "Bibi", "color": "blue" }, 
	"opts": { "roomCapacity": 3, "public": false, id: "my id", "nbMonsters": 4 }
}

(The playerOpts.color and opts.nbMonsters parameter are just here to help you remember that you can add any additional parameter you need to your game)

Get the response

Once you connected a player to a room, this event contains the status of the connection and the id of the room to which you connected to.

Content:

  • status: "OK"|"KO"
  • message: string
  • id: string (if private)
  • event: "response"

If you use socket.IO, you will have to listen on the response event.

The obtained id is the string other connections will need to join the game (You can, for e.g., add it at the end of the URL of your game and obtain a route like http://my-game/:id).

Private messages & sub-group messages

Private message

export class MyWonderfulChat extends ARealtimeGame<ChatUser>
{
	constructor()
	{
		//super, join & receive_message event here
		this.on("receive_private_message", (packet: PrivateMessagePacket, emitter: ChatUser) => {
			this.apply((p) => {
				if (packet.username !== p.username)
					return;
				p.send<MessagePacket>("send_private_message", {
					message: `**${emitter.username}**: ${packet.message}`
				});
			});
		});
	}
}

interface PrivateMessagePacket extends MessagePacket
{
	username: string;
}

Packet documentation

You could ask yourself "Why I must always extends IReceivedPacket/ISendPacket classes for each of my packet, that's boring !".

The reason is simple: It will be much simpler for you to create documentation of each of your packet in this way. You'll exactly know what they contain, it limits a lot the surprises you could have with undefined terms (which can always occurs if front-end send a wrong packet, but at least your packets were documented).

Note: You are not concerned if you use the JS version

Apply & filter

this.apply apply the callback passed as paramater to every player in the PlayerList.

A last thing: To avoid apply to all players, if your PrivateMessagePacket send the id of the player instead of its username, you can do:

const p = this.getPlayer(packet.receiverId);

if (p) {
	p.send("send_private_message", <MessagePacket>{
		message: `**${emitter.username}**: ${packet.message}`
	});
}

this.getPlayer lets you access to a player through its id, generated by the server in the APlayer class (and, as a reminder: The class you create to represent the player MUST extends the APlayer class).

Sub-group message

Let's transform our chat to make it becomes a LGeL chat game

export class ChatUser extends APlayer
{
	// Let's assure a player has a 33% chances to be a werewolf.
	public readonly isWerewolf: boolean = Math.random() < 0.33;
	public readonly username: string;

	constructor(opts?:any)
	{
		super();
		this.username = opts?.username || "guest";
	}
};

export class MyWonderfulChat extends ARealtimeGame<ChatUser>
{
	constructor()
	{
		//super, join, receive_message, private message events here
		this.registerReceiveEvent("receive_lg_message", (packet: MessagePacket, emitter: ChatUser) => {
			if (!emitter.isWerewolf)
				return;
			this.filter((p) => p.isWerewolf).apply((p) => {
				p.send<MessagePacket>("send_lg_message", {
					message: `**Anonymous werewolf**: ${emitter.messager}`
				});
			});
		});
	}

	protected onJoin(p: ChatUser)
	{
		this.broadcast<MessagePacket>("send_message", { 
			message: `**${p.username}** joined the room.`
		});
	}
	protected onDisconnect(p: ChatUser)
	{
		this.broadcast<MessagePacket>("send_message", { 
			message: `**${p.username}** left the room.`
		});
	}
	protected run()
	{
		this.apply((p) => {
			p.send<PlayerInfosPacket>("start", { isWerewolf: p.isWerewolf });
		});
	}
	protected close()
	{

	}
}

interface PlayerInfosPacket extends ISendPacket
{
	isWerewolf: boolean;
}

- "What are thoses "run" and "close" functions ?

In fact, I didn't wrote them in the examples below, but your program may not compile or crash if you don't use them.

The this.run function is called when the game room is filled. It is a bit like the "entry point" or the "main" of your game.

The this.close function is called when you call the this.stop function (yes, it is up to you to define when/if a game end, so you have to tell this to the core by calling this function).

It is called just before all sockets are destroyed, and just before the game room is deleted (At this point you MUST create this function, but it may be facultative in future releases).

- What are thoses "onJoin" and "onDisconnect" functions ?

The onJoin function is triggered each time a new player joins the room. After its initialization, the player object is sent as parameter of the function.

The onDisconnect function is triggered each time a player leaves the room. The player object is sent as parameter of the function.

- "What is the difference between this.on and this.registerReceiveEvent ?"

this.registerReceiveEvent will only triggers its callbacks after the game starts, so it let you make the difference between your "game" events and your "i-can-be-used-at-any-time" events (In fact the usage of this.on can be avoided if your players don't need to send anything before the game starts, and it is generally only used for a chatroom system).

A Full example

import { APlayer, ARealtimeGame, ISendPacket, IReceivedPacket } from "etwin-socket-server";

//Suggested in a ChatUser.ts
export class ChatUser extends APlayer
{
	public readonly isWerewolf: boolean = Math.random() % 3 < 1;
	public readonly username: string;
	
	constructor(opts?: any)
	{
		super();
		this.username = opts?.username || "guest";
	}
}

//Suggested in a MyWonderfulchat.ts
export class MyWonderfulChat extends ARealtimeGame<ChatUser>
{
	private static readonly MAX_SIMULTANEOUS_CONNECTIONS = 10;
	
	constructor()
	{
		super(MyWonderfulChat.MAX_SIMULTANEOUS_CONNECTIONS);
		
		//On new global message received
		this.on("receive_message", this.onReceiveMessage);
		
		//On new message sent
		this.registerReceiveEvent("receive_lg_message", this.onReceiveLgMessage);

		//On private message
		this.registerReceiveEvent("receive_private_message", this.onReceivePrivateMessage);
	}

	private onReceiveMessage(packet: MessagePacket, emitter: ChatUser)
	{
		this.broadcast("send_message", <MessagePacket>{
			message: `**${emitter.username}**: ${packet.message}`
		});
	}
	private onReceiveLgMessage(packet: MessagePacket, emitter: ChatUser)
	{
		if (!emitter.isWerewolf)
			return;
		this.filter((p) => p.isWerewolf).apply((p) => {
			p.send<MessagePacket>("send_lg_message", {
				message: `**${emitter.username}**: ${packet.message}`
			});
		});
	}
	private onReceivePrivateMessage(packet: PrivateMessagePacket, emitter: ChatUser)
	{
		this.filter((p) => packet.username === p.username).apply((p) => {
			p.send<MessagePacket>("send_private_message", {
				message: `**${emitter.username}**: ${packet.message}`
			});
		});
	}

	protected onJoin(p: ChatUser)
	{
		p.send<MessagePacket>("connection_established", {
			message: `Hello to you and welcome to the chat, ${p.username}!`
		});
		this.broadcast<MessagePacket>("new_player_connected", {
			message: `**${p.username}** joined the chat!`
		});
	}
	protected onDisconnect(p: ChatUser)
	{
		this.broadcast<MessagePacket>("send_message", { 
			message: `**${p.username}** left the room.`
		});
	}
	protected run()
	{
		this.apply((p) => {
			p.send<PlayerInfosPacket>("start", { isWerewolf: p.isWerewolf });
		});
	}
	protected close()
	{
		this.broadcast<MessagePacket>("close" {
			message: "Room was closed. Good bye"
		});
	}
}

//Suggested in a MyGamePackets.ts
interface PlayerInfosPacket extends ISendPacket
{
	isWerewolf: boolean;
}
interface MessagePacket extends IReceivedPacket, ISendPacket
{
	message: string;
}
interface PrivateMessagePacket extends MessagePacket
{
    username: string;
}

//Suggested in an index.ts
async function main(): Promise<void>
{
	const ss = new SocketServer(MyWonderfulChat, ChatUser);

	ss.createServer(SocketServer.Type.IO, 3000);
	ss.createServer(SocketServer.Type.TCP, 3001);
	ss.run();
}

main().catch((err: Error) => {
	console.log(err.stack);
	process.exit(1);
});
1.7.3

2 years ago

1.7.6

2 years ago

1.7.5

2 years ago

1.7.4

2 years ago

1.7.2

2 years ago

1.7.1

2 years ago

1.7.0

2 years ago

1.6.4

3 years ago

1.6.3

3 years ago

1.6.5

2 years ago

1.6.2

3 years ago

1.6.1

3 years ago

1.6.0

3 years ago

1.5.3

3 years ago

1.5.32

3 years ago

1.5.31

3 years ago

1.5.2

3 years ago

1.5.1

3 years ago

1.5.0

3 years ago

1.4.2

3 years ago

1.4.21

3 years ago

1.4.1

3 years ago

1.4.0

3 years ago

1.3.0

3 years ago

1.2.6

3 years ago

1.2.5

3 years ago

1.2.4

4 years ago

1.2.3

4 years ago

1.2.2

4 years ago

1.2.1

4 years ago

1.2.0

4 years ago

1.1.0

4 years ago

1.0.4

4 years ago

1.0.3

4 years ago

1.0.2

4 years ago

1.0.1

4 years ago

1.0.0

4 years ago