Game maker studio 2 tcp netwroking server.
Everything you need to compile the server is at the Auth Server folder.
At the release tab, you will find the complete library for game maker studio 2 and hovering the functions will explain how everything whorks
The server was built for anyone to create their own packets and logic, all you need to know is a basic c# coding logic and patience to test everything :)
Special thanks to FatalSheep and CinderFire for the old basic networking tutorial at the Game maker studio forum (rest in peace tutorial.. XD) And an special thanks to @jujuadams for HMAC SHA256 Hashing scripts!
In order to compile the server you will need to install Visual Studio 2022 Community edition and use .net8.0, which already comes with the latest version of it.
The server is prepared to be compiled either for x86 or x64 bits processors, and it should be able to run it on windows, linux, and macOs systems without further problems.
Usually data packets, from game maker and any other server that uses any kind of buffer stream or udp stream of data, comes to the server in a Hexadecimal composition. Packets are essentially hexadecimal data streams and the server is resposible to decrypt it and turn it into readable data, so we can manipulate it more easily.
The packet in hexadecimal is made by 3 sections:
- packet id;
- encryption header; β
- packet data;
- end of the packet;
Let's suppose that i wanna make a packet, its number will be number 1000, theres 3 crypt headers: packet header string; timestamp like this: 1722855600000, in miliseconds; a generated key to check. The packet content data is composed by one string: hello world.
The hexadecimal for this packet will be: 03 E8 30 78 30 33 45 38 31 37 32 32 38 00 35 35 36 30 30 30 30 30 43 45 44 43 43 41 37 32 32 31 33 38 30 44 38 38 37 41 33 34 43 45 37 41 32 37 44 35 34 39 43 46 46 42 43 31 44 41 43 39 33 44 36 35 33 45 35 32 33 36 30 36 33 31 37 44 38 31 38 35 41 34 35 36 68 65 6C 6C 6F 20 77 6F 72 6C 64 00 00.
The first part of the packet: 03 E8 it means the number of the packet which is the number 1000 but in decimal, next we have the 30 78 30 33 45 38 which means the '0x03E8' packet hex or 1000 in decimals but turned into an string, next we have the 35 35 36 30 30 30 30 30 which means '1722855600000' that represents the timestamp in miliseconds, and the last but not least, 68 65 6C 6C 6F 20 77 6F 72 6C 64 which means 'hello world', and the last 00 it's a blank space which means the end of the packet.
Note that the packet strings needs to be a multiple of 4 bites, example: 'Test' => 74 65 73 74 which indeed is multiple of 4, but since string bites has an terminator \0 at the end of the string, we need to increment the \0 terminator bite at the end, so its 5 bites which is not a multiple of 4, in that case we should increment bites until its multiple of 4, like this: 'Test' => 74 65 73 74 00 00 00 00 wich the first 00 is the terminator and the rest of them should act like an offset for the next data read.
With this knowledge you can pretty much understand how packets is made of, and you can also create your own raw packets using hexadecimals, without using the game maker client to do so.
As for the encryption part, you should build an encrypter in order to receive the correct crypt hash, if you gonna do this manually without using GameMaker and the SN library.
Note that between 'hello' (68 65 6C 6C 6F) and 'world' (77 6F 72 6C 64) theres a 20 value which means the space character, it differs from 00 which is literally blank, so if you're gonna adventure yourself in making raw packets, take a note that those two hex are different, one for string value and another for ending the packet or data stream.
Creating packets is simple and heres the tutorial of it.
At the client side, you can call the the function sn_send_new_packet(); or sn_send_new_nodata_packet(); from the library, depending on what you wanna do, either send a signal packet with no data, or a packet with data inside.
For the sn_send_new_packet(); , which includes data, you need to pass a function inside like this:
// Packet with informations to send to the server.
// You can use any parameter name inside the function, but remember that whatever is the name you chose, it will have the buffer object created by the sn_send_new_packet() inside it.
sn_send_new_packet(function (_packet) {
buffer_write(_packet, buffer_string, "admin"); // <-- First the buffer provided by the main function, second the data type (buffer_string, buffer_u8 up to 64, buffer_f8 up to 64, buffer_bool, buffer_text) and next it's value.
buffer_write(_packet, buffer_string, md5_string_unicode(12345678)); // <-- i'll send another one too, but this time its the password already formated and converted to an MD5 hash.
}, 0xA001, "0xA001");// <-- the 0xA001(Number 40961) hex is the packet identifier (packet header) you gonna add on the server side so the server can process the data you sent.
// the string "0xA001" as the last argument for this function, is needed in order to validade the packet at the encryt validation module inside the client, and it should be the same as the packet header number.
// And... thats it, the script already handles everything for you, including the encryption of the packet, and now, you just need to treat it on the server side.Now, to treat the packet that's coming from the client, on the server side, you just need to do something like this:
// At the Auth.cs class you're gonna find the "OnPacketReceived" function, and inside the switch you gonna add a new case with the same packet id (packet header) you chose on the client.
case 0xA001:
{
// First of all, we should first verify the encryption for the packet and Decrypt it in order to proceed with the operation,
// otherwise the server will automatically disconnect the player for packet cracking purposes.
// This function here checks whether the encrypted key valid or not. We should pass the same packet header as an string in order to check.
if (!ValidateClientKeyOrDisconnect("0xA001", sender, readBuffer)) break;
// For some reason, and i think its something related to the ValidateClientKeyOrDisconnect function, or the encrypt function at the client,
// whe should skip the first 3 strings from the packet header, which represents the encrypt packet.
// I should find an workaround for this sooner.
readBuffer.Read(out string _);
readBuffer.Read(out string _);
readBuffer.Read(out string _);
// Now that we got thorught the validation of the encryption for the packet head to the content of the packet first you chose the variable type, since we sent a string on the client, the variable must be the same.
// Note that the buffer will read the packet from the start, on the client we sent one string and one int value, so we should declare the variables in the same order, like below:
readBuffer.Read(out string user);
readBuffer.Read(out string password); // <-- note that the int is a 16bits integer type, the same we sent from the client.
// here we can send those informations to the server who's responsible for processing this data, in this case, the login server
var loginServer = GetRegisteredServersByType(ServerType.LoginServer);
if (loginServer != null)
{
BufferStream buffer = new BufferStream(GetBufferSize(), GetBufferAlign());// Start a new packet for the giving minimun size and alignment (should be the same as game maker's client and at the other csharp server too.)
buffer.Seek(0); // <-- Start the packet from the position 0
buffer.Write((ushort)0xB100);// <-- Write the packet number to the start of the packet you're gonna send (And its important to be the first thing you write since the server and client seeks for it in position 0);
buffer.WriteWithoutIterate(user);// <-- you must put everything after the 0xA002 entry, and at the server side, you need to read it at the same order.
buffer.WriteWithoutIterate(password);
buffer.WriteWithoutIterate(sender.ClientIPAddress); // <-- here whe should send the client IP address to the login server for log purposes or for further invalidation purposes.
SendMessage(buffer);// <-- send it to the login server.
}
else
{
// Just a little error treatment for the socket if its not found, sending an packet back to the client
// Just noticed that this packet doesnt have an encryption header attached to it, which will be fixed in version v0.3.
Console.WriteLine("[Auth::LoginRequest] No login server found");
var err = new BufferStream(GetBufferSize(), GetBufferAlign());
err.Seek(0);
err.Write((ushort)0xB001);
// Encryption here.
err.WriteWithoutIterate("Login Server was offline or closed for maintenance.");
sender.SendMessage(err);
break;
}
}In a real scenario, the auth server will only be responsible to authenticate the oprations. In order to process the packet above, we need to make the same thing we did here but in the Login Server at the Server.cs.
Note that between CSharp servers, theres no need to skip the first 3 encryption strings of the packet, this is only when receiving data from the client, or else the server will get confused in a way that he will stop reading the packets properly since he thinks that th packet is still encrypted somehow.
A example for the code i've shown earlier, which send the user and password to the login server, should be done at the Server.cs of the Login Server like this:
case 0xB100:
if (!ValidateClientKeyOrDisconnect("0xB100", sender, buffer)) break;
// Reading from packeet
buffer.Read(out string username);
buffer.Read(out string password);
buffer.Read(out string client_IP);
// Prevention against flooding attacks
if (IsFlooding(client_IP))
{
Console.WriteLine($"[Server::FloodPrevention] Flood detected from {client_IP}. Connection rejected.");
// Sending the flood login packet to the auth server
var floodBuffer = new BufferStream(BufferSize, BufferAlign);
floodBuffer.Seek(0);
// --Packet Header
floodBuffer.Write((ushort)0xA005);
// --Security Header
long floodPacketTimestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
var flooodPacketKey = _crypt.GenerateKey($"0xA005|{floodPacketTimestamp}");
floodBuffer.Write("0xA005");
floodBuffer.Write(floodPacketTimestamp.ToString());
floodBuffer.Write(flooodPacketKey);
// --Packet content
floodBuffer.Write(client_IP);
sender.SendMessage(floodBuffer);
break;
}
// More methods will be added soon, such as: Wrong password flood block, blacklist IPs, etc.
// If theres no flood then continue with the login process
// For now, we only return that the login was successful since we dont have any database yet
var successLoginBuffer = new BufferStream(BufferSize, BufferAlign);
successLoginBuffer.Seek(0);
// --Packet Header
successLoginBuffer.Write((ushort)0xB003);
// --Security Header
long successLoginTimestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
var successLoginPacketKey = _crypt.GenerateKey($"0xB003|{successLoginTimestamp}");
successLoginBuffer.Write("0xB003");
successLoginBuffer.Write(successLoginTimestamp.ToString());
successLoginBuffer.Write(successLoginPacketKey);
// --Packet content
successLoginBuffer.Write(client_IP);
sender.SendMessage(successLoginBuffer);
break;this should return to the Auth server, like this:
case 0xB003: // LoginResponse vindo do LoginServer
if (!ValidateClientKeyOrDisconnect("0xB003", sender, readBuffer)) break;
// Reads the response from the LoginServer
readBuffer.Read(out string client_IP);
//Later, we can maybe get the nickname defined by the player, or do any other kind of processing based on player's info on database and report back to client.
//Checks if the client IP is registered
var client_socket = FindClientByIP(client_IP);
if (client_socket != null)
{
var response = new BufferStream(GetBufferSize(), GetBufferAlign());
response.Seek(0);
// --Packet Header
response.Write((ushort)0xB101);
// --Security Header
string responseSucessLoginTimestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString();
string responseSucessLoginPacketKey = crypt.GenerateKey($"0xB101|{responseSucessLoginTimestamp}");
response.WriteWithoutIterate("0xB101");
response.WriteWithoutIterate(responseSucessLoginTimestamp);
response.WriteWithoutIterate(responseSucessLoginPacketKey);
client_socket.SendMessage(response);
}
else
{
Console.WriteLine();
Console.WriteLine($"// ------------------ [Auth::OnPacketReceived] ERROR ----------------------- //");
Console.WriteLine($"// -- No client socket where found for IP: {client_IP}");
Console.WriteLine($"// ------------------------------------------------------------------------- //");
}
break;And last but not least, inside the client (Object Obj_NetController on game maker), you can go to the Asyncronous Networking action and inside the piece of code you will find all the packets that the client can understand.
You just need to add the packet number 1025 you sent from the server at the switch, like this:
case 0xB101:
{
if(sn_validate_packet_encryption(_read_buffer))
{
// Here we can proceed like e want..
// Here whe close the wait message window and proceed to the next window
// Or we can just move to another room, maybe.
with(ObjWaitWindow) {
can_close = true;
can_open_another = true;
open_next_window = ObjWindowObject;
}
}
else
{
// Disconnect player.
// If, somehow, the encryption validation was failed, the client self-destroy the socket at the client, making the player disconnect for invalid crypt key.
with(ObjWaitWindow) can_close = true;
instance_destroy(); // <-- here we already send a disconnection packet.
game_end(0);// <-- Finish the game after that, it should lead to an disconnection notice but for now it ends the game.
}
break;// <-- Remember that for packets such as Pings and informations that the server needs to constantly send to the client and vice versa, the break should not be declared.
}And thats how you set up packets between the server and client! :)
The server has some basic features, security enhancements and some more gimmics since v0.2 was being released, but at this point you can only use the Auth Server and Login Server. Game server will be added to update v0.3 along with some other feautres and security enhancements.
The following features comes along with both servers and some client side changes, such as:
- Complete TCP Handler;
- Basic Packet Decrypting (later on, it will really decrypt the entire packet and not just the header validation); β
- Basic Packet Encrypting (later on, it will really encrypt the entire packet and not just writing security keys at the header); β
- BufferStream system with a new non-interation write string method for GameMaker buffer system compatibility with strings not multiple of 4 bites. β
- Packet list.
- Disconnection handler now features a better handler for disconnection. β
- Ping handler. (distance to server average + true ms from each packet sent). β
- Initial packet for connection.
- IP address from the client now can be gathered by the server. β
- New GUI implementation and animations. β
- SSBB sfx only for testing purposes, but you should put your own. β
- New server for handling the Login procedures and validations for the client. β
- New scripts for Encryption, UI Forms, Better server register section and more. β
- Several bugfixes, such as bugs with window animation, double packet delivery, high ping measurement, performance issues, etc.
Any questions, please drop a message at my discord: yuuto.x or even drop a message at my facebook: @varlocyt