Game Development Community

Master Server Query System

by Robert Fritzen · in Torque 3D Professional · 07/11/2013 (6:09 pm) · 11 replies

Hello everyone, I have a question that I'm sure a few people know the answer to.

I'm currently working on an expansion to my first pack to include a new module to replace the existing master server query system with one that a PHP solution would work with. I already have the network tools to communicate data between the game client and the PHP server, as well as the new heartbeat code for servers to communicate with the PHP solution.

Like most of my good programming practices, a good lesson of mine is to find existing code that works, and pick it apart step by step to learn how it works in order to write a new solution (IE: How I did HazardPrecipitation). Unfortunately, the serverQuery file is a heck of a lot more confusing than anything I have had to look at so far with Torque 3D, and I think part of the problem is the ancient-ness of the code. So, I'm looking for old engine vets who know the system to lend a hand here.

What I would like to know is how the NetInterface class is actually supposed to function. I have a new class that uses code from the existing system. I'm able to pull the list of IP's down from the server and get it into a proper format (IE: NetAddress) for reading by the old system, but my problem is that there are a ton of different underlying code fragments inside the old serverQuery.cpp file relating to getting data from the master server, the server filters, and other stuff.

My current code can get the list of servers, and it initiates server ping, but it immediately says the server query is done with no servers found with no ping retries.

Does anyone know how the order of code works. Basically a flowchart perspective, from when the query button is pushed to getting servers on the GUI screen for joining, what functions are called and how does it move around?

I would be very grateful for any information that can help me out with this problem.

#1
07/11/2013 (7:00 pm)
Here's what I know, Robert. This will probably require multiple posts so bear with me. TL;DR incoming!

Some basic info first:

1). The game keeps track of the list of master servers in the $pref::Master[0-n] array. It typically looks something like:
$pref::Master[0] = "2:my-master.server.com:28002"

The format for it is: (U32)<region_id>:<server-address>:<server-port>

<region_id> is supposed to be used so as a sort of master server filter. Players could select their region and query only the master server(s) that are physically closer to them than others, thereby (hopefully) being able to communicate with it faster. It might also be possible for game servers to register only with specific master servers, so, say, game servers in Europe may only register with the European master server.

<region_id> is also entirely arbitrary. The U.S. could = 1, Europe = 2, etc. Couple of caveats, though: a). <region_id> has to be > 0 and b). only one master server per region is allowed.

<server-address> is simply a string that describes an IPv4 address.

<server-port> is the port the master server is listening on.

2). The game uses the MasterInfo struct to store info about a specific master server. The MasterInfo struct just contains a NetAddress object that describes the server's IP address/port info and a U32 for the server's region id/mask.

The game stores a list (well, Vector... this is Torque, after all :P) of every master server the client knows about (whatever is in the $pref::Master[] array). This list is cleared and refilled every time the player queries a master server, every time a game server receives a heartbeat request, etc.

#2
07/11/2013 (7:33 pm)
Game Server -> Master Server Communication:

When a game server is initially created, the script function createServer() is called. This function also checks the $pref::Net::DisplayOnMaster variable and, assuming it != "Never", schedules a call to the C++ function startHeartbeat().

The startHeartbeat() function does a few things:

1). It increments gHeartbeatSeq. This variable is used in processHeartbeat() as a way of ensuring only current heartbeat 'ticks' are processed. Any outdated/late heartbeat requests are dropped.

2). It calls processHeartbeat(). More on this in a bit.

3). At one time, it also validated a game server's authentication by calling validateAuthenticatedServer(). I assume this was used in Tribes 2 (maybe some proprietary stuff for the WON.net network?) because the code for it is long gone and the function just returns true.

Now, processHeartbeat(). This function simply checks to make sure the heartbeat 'tick' is current (gHeartbeatSeq == passed in 'seq' param). If it is, processHeartbeat() calls sendHeartbeat(0). Lastly, it schedules itself to be called again in 2 minutes. So every 2 minutes a game server will send out a heartbeat event to all master servers.
#3
07/11/2013 (7:58 pm)
Client -> Master Server Communication:

A player can ask any/all master servers for a list of game servers by calling the function queryMasterServer(). This function takes a few params that essentially act as a filter against any game servers registered with the master server. Only servers that meet the player's criteria will be returned:

U8 flags = corresponds to ServerFilter::queryFlags, should be 0 typically

const char* gameType = just the string name of a game type

const char* missionType = same as above, just the string name of the mission type

U8 minPlayers = min players allowed on the server

U8 maxPlayers = max players allowed on the server

U8 maxBots = unused, was max AI bots allowed on the server in Tribes 2

U32 regionMask = region id of the server (so players can filter servers by region)

U32 maxPing = max ping of server (from server -> master server) allowed

U16 minCPU = min CPU speed of the server

U8 filterFlags = used as a bitmask for a number of different server features, including: is the server dedicated, passworded, running on linux, running the current version of the game, and a xenon build; seems to be almost entirely unused in the scripts

U8 buddyCount = unused, always 0

U32* buddyList = unused, always 0

All of this data is packaged together and sent to the master server(s) in the processMasterServerQuery() function. It uses the BitStream class to send the actual data. This will likely be an important piece of code for you to review so you know how the query packets are formed, what data they contain and in what format.
#4
07/11/2013 (8:25 pm)
Master Server -> Client Communication

Once a master server receives a 'MasterServerListRequest' packet from a client (fyi: all of the different packet types can be found in NetInterface::PacketTypes), the master server goes through a basic process of communicating with each server to determine if it passes the client's filter(s) (such as CPU speed, player count, etc.). All of the accceptable servers are then sent (individually I believe) to the client for processing via the handleMasterServerListResponse() and processServerListPackets() functions.


That's about as much info as I can think of. If you have specific questions I may be able to answer those. All of this is really me just throwing stuff out and hoping some of it helps.

I will say, if you're able to actually retrieve a list of servers but having trouble pinging them a good place to start will be in the handleGamePingResponse() function.
#5
07/11/2013 (8:26 pm)
This is helpful information, thanks for that.

My current code works for the server -> master communication portion. I'm looking to finish the client -> master portion.

What I have currently is code that gets a list of active servers from PHP, and places that list in a vector of ServerInfo *. I'm trying to figure out what I need to do next. The code says it's pinging the servers, but for some reason it doesn't actually appear to be doing so. I'm just wondering what pieces of code I absolutely need and how they interact with each other so I can understand the system.
#6
07/11/2013 (8:47 pm)
Are you getting a list of valid servers (correct IP addresses/ports)?

Ping requests will be ignored if the server is marked as SinglePlayer, if the server isn't running a mission, or if the ServerFilter::OfflineQuery flag is set. handleGamePingRequest() is called on the server when a ping request is received. That'd probably be a good place to step through.
#7
07/14/2013 (2:21 pm)
Ok, so what I have so far.

The list comes in the proper format:

IE: "IP:xxx.xxx.xxx.xxx:xxxxx", obviously with numbers.

I then get output like this:
==>queryAllServers();
* Constructing ServerQuery Filter.
* ServerQuery Filter Constructed
* Querying master server
Pinging Server IP:98.213.147.84:28000 (3)...
* Querying local servers
ServerQuery:  start Querying LAN servers 0
LAN server ping: IP:Broadcast:28000...
Pinging Server IP:98.213.147.84:28000 (2)...
ServerQuery:  ping Pinging servers: 1 left... 0
Pinging Server IP:98.213.147.84:28000 (1)...
Pinging Server IP:98.213.147.84:28000 (0)...
Ping to server IP:98.213.147.84:28000 timed out.
ServerQuery:  query Querying servers: 0 left... 0.5
ServerQuery:  done No servers found. 1
ServerQuery:  done No servers found. 1

Notice how the master query ignores the request completely, then the local request proceeds to handle it normally.

I'm pretty sure the server is marked as MultiPlayer,and allowConnections set to true. I'll double check to see if the settings are being overwritten anywhere, but I doubt it. Lastly, the filter system is currently disabled in my system so it shouldn't cause that flag to be set off (even though the filer is default to OnlineQuery anyways if it IS on).

I have added echoes to each of the handle callbacks, and none are being set off by this request.
#8
07/27/2013 (10:39 pm)
Oops, sorry Robert, I completely forgot about this thread.

Not sure what progress you've made in the last two weeks. Does the server at 98.213.147.84 register with GG's master server? Does it respond to pings at all?

To be honest, the whole master server and serverQuery stuff needs to be gutted and rewritten. It has a lot of Tribes 2-era cruft in it that really over-complicates things.

Having master servers spread across regions that send filtered server lists directly to players made sense in the T2 days because there were thousands of active servers across the globe and the majority of the userbase was still on dial-up so you had to be careful about bandwidth with even simple things like this. That's no longer the case, though. The vast majority of multiplayer indie games will be lucky to have a handful of active servers, don't have the budget (or need) to support region-specific master servers and most players have broadband connections.

A new, simpler serverQuery system & corresponding master server would actually be a really worthwhile effort now that I think about it. All a master server really needs is the ability to store server addresses and be able to respond to certain RPCs. That info could then be sent anywhere, to a game UI, to a webpage, etc. Filtering and sorting could/should be done on the client so there's no need to worry about that.
#9
07/28/2013 (7:39 am)
My Progress, I figured out what was going on with my code, which actually was working fine, it was still using the old DemoNetInterface being declared by serverQuery.cpp, so I just made a toggle flag for it and my system worked perfectly fine.

I do agree that a complete re-write of the system is needed, but for now I'm not quite able to tackle that challenge.
#10
11/06/2014 (5:18 pm)
hey robert how did you fix the time out issue?

Pinging Server IP:98.213.147.84:28000 (1)...
Pinging Server IP:98.213.147.84:28000 (0)...
Ping to server IP:98.213.147.84:28000 timed out.
ServerQuery: query Querying servers: 0 left... 0.5
ServerQuery: done No servers found. 1
ServerQuery: done No servers found. 1

im using your resource for master server with php/mysql and i have it working to the point where the host registers in the database and everything but when the client queries the master server it gets the timeouts and it doesn't show the server. thanks
#11
11/07/2014 (10:14 am)
The timeout issue is due to the functioning of the querySingleServer function, which actually cannot directly ping a new server, only update existing ones on the list.

I wrote a post about this a while back, I believe there's some code in there to fix the problem, but you'll need to do some digging on these forums to find it.

Edit: Ignore the digging part, found it: here.