[twilight-commits] r10414 - in trunk/dpmaster: . doc src testsuite

molivier at icculus.org molivier at icculus.org
Sun Aug 22 13:06:17 EDT 2010


Author: molivier
Date: 2010-08-22 13:06:16 -0400 (Sun, 22 Aug 2010)
New Revision: 10414

Added:
   trunk/dpmaster/testsuite/test-new_heartbeat.pl
   trunk/dpmaster/testsuite/test-unknown_heartbeat.pl
Modified:
   trunk/dpmaster/doc/techinfo.txt
   trunk/dpmaster/readme.txt
   trunk/dpmaster/src/common.h
   trunk/dpmaster/src/dpmaster.c
   trunk/dpmaster/src/games.c
   trunk/dpmaster/src/games.h
   trunk/dpmaster/src/messages.c
   trunk/dpmaster/src/servers.c
   trunk/dpmaster/src/servers.h
   trunk/dpmaster/src/system.c
   trunk/dpmaster/testsuite/query_remote_master.pl
   trunk/dpmaster/testsuite/test-gametype.pl
   trunk/dpmaster/testsuite/test-multiple_game_families.pl
   trunk/dpmaster/testsuite/testlib.pm
Log:
Fixed a bug that prevented daemonization when chrooted (thanks to LordHavoc for diagnosing this one). Fixed a warning when building with GCC. New system for managing game properties, including built-in support for "Return to Castle Wolfenstein" and "Wolfenstein: Enemy Territory". Shutdown heartbeats and unknown heartbeats are now ignored. Fixed the game type when printing the server list in the log

Modified: trunk/dpmaster/doc/techinfo.txt
===================================================================
--- trunk/dpmaster/doc/techinfo.txt	2010-08-22 16:52:33 UTC (rev 10413)
+++ trunk/dpmaster/doc/techinfo.txt	2010-08-22 17:06:16 UTC (rev 10414)
@@ -179,17 +179,18 @@
 
         - samples:
 
-            "\xFF\xFF\xFF\xFFheartbeat QuakeArena-1\x0A" (Q3A protocol)
-            "\xFF\xFF\xFF\xFFheartbeat DarkPlaces\x0A"   (DP protocol)
+            "\xFF\xFF\xFF\xFFheartbeat DarkPlaces\x0A"       (DP protocol)
+            "\xFF\xFF\xFF\xFFheartbeat QuakeArena-1\x0A"     (Q3A)
+            "\xFF\xFF\xFF\xFFheartbeat Wolfenstein-1\x0A"    (RtCW)
+            "\xFF\xFF\xFF\xFFheartbeat EnemyTerritory-1\x0A" (WoET)
 
         - syntax:
 
             A protocol string is required after the type name, and a line feed
             (character 10, '\n') closes the message. Note that the string after
             the type name is a PROTOCOL STRING, not a game string! Please make
-            sure you use "DarkPlaces" as your protocol string. Dpmaster only
-            checks this string for debugging purpose for the moment, but it's
-            likely to change in the future. You're warned.
+            sure you use "DarkPlaces" as your protocol string if you use
+            dpmaster for your own game, or it won't work!
 
     2) getinfo:
 
@@ -234,8 +235,9 @@
             "challenge"). "sv_maxclients" (the maximum number of clients
             allowed on the server; must not be 0), "protocol" (the protocol
             number) and "clients" (the current number of clients on the
-            server) must also be present. Except for Q3A, "gamename" (the game
-            name) is mandatory too, and its value must not contain whitespaces.
+            server) must also be present. Except for anonymous games like Q3A,
+            "gamename" (the game name) is mandatory too, and its value must not
+            contain whitespaces.
             
             Starting from version 2.0, dpmaster also uses the value of the
             "gametype" key, which may be specified as a filter in server list
@@ -256,15 +258,18 @@
         - samples:
 
             "\xFF\xFF\xFF\xFFgetservers 67 ffa empty full" (Q3A)
+            "\xFF\xFF\xFF\xFFgetservers 84"                (WoET)
             "\xFF\xFF\xFF\xFFgetservers Nexuiz 3"          (DP running Nexuiz)
             "\xFF\xFF\xFF\xFFgetservers qfusion 39 full"   (QFusion)
 
         - syntax:
 
-            The message must contain a protocol version, and optionally "empty"
+            The message must contain a protocol number, and optionally "empty"
             and/or "full" depending on whether or not the client also wants to
-            get empty or full servers. Except for Q3A, the client has to specify
-            its game name, right before the protocol number.
+            get empty or full servers (except for WoET, which always expect a
+            list of all servers). A client using the DP protocol also has to
+            specify its game name, right before the protocol number. Anonymous
+            games such as Q3A or RtCW don't, obviously.
 
             Starting from dpmaster version 2.0, you may also add in the option
             list at most one gametype filter "gametype=X", where X is the

Modified: trunk/dpmaster/readme.txt
===================================================================
--- trunk/dpmaster/readme.txt	2010-08-22 16:52:33 UTC (rev 10413)
+++ trunk/dpmaster/readme.txt	2010-08-22 17:06:16 UTC (rev 10414)
@@ -13,10 +13,11 @@
  5) OUTPUT AND VERBOSITY LEVELS
  6) LOGGING
  7) GAME POLICY
- 8) ADDRESS MAPPING
- 9) LISTENING INTERFACES
-10) VERSION HISTORY
-11) CONTACTS & LINKS
+ 8) GAME PROPERTIES
+ 9) ADDRESS MAPPING
+10) LISTENING INTERFACES
+11) VERSION HISTORY
+12) CONTACTS & LINKS
 
 
 1) INTRODUCTION:
@@ -25,8 +26,9 @@
 game engine DarkPlaces. It is an open master server because of its free source
 code and documentation, and because its Quake III Arena-like protocol allows it
 to fully support new games without having to restart or reconfigure it: start
-and forget. In addition to its own protocol, dpmaster also supports the original
-Quake III Arena master protocol.
+and forget. In addition to its own protocol, dpmaster also supports the master
+protocols of "Quake III Arena" (Q3A), "Return to Castle Wolfenstein" (RtCW), and
+"Wolfenstein: Enemy Territory" (WoET).
 
 Several game engines currently support the DP master server protocol: DarkPlaces
 and all its derived games (such as Nexuiz and Transfusion), QFusion and most of
@@ -223,8 +225,114 @@
 called "-v" (certainly not what you want...).
 
 
-8) ADDRESS MAPPING:
+8) GAME PROPERTIES:
 
+Dpmaster supports 2 kinds of games: open-source games which use the DarkPlaces
+master protocol, and a few formerly closed-source games which use the Quake 3
+master protocol or a variant of it. The DarkPlaces master protocol itself is a
+variant of the Quake 3 master protocol, the main difference being that games
+send their name in addition to the usual informations or queries. That's what
+makes dpmaster able to support multiple games easily.
+
+Unfortunately, formerly closed-source games don't always send this information,
+or another information that allows dpmaster to guess the game name safely.
+That's why we call them "anonymous games" here. Up to version 2.1, the only
+anonymous game dpmaster supported was Q3A, so it was easy: if the game didn't
+send its name, it was Q3A. But starting from version 2.2, dpmaster also supports
+2 other anonymous games: RtCW and WoET. That's why a new mechanism had to be
+created to allow dpmaster to figure out which game sends it which message. This
+mechanism is called "game properties".
+
+Game properties are controlled by the command line option "--game-properties"
+(short option: "-g"). A number of properties are built into dpmaster, so you
+shouldn't have to configure anything for a standard usage. You can make it print
+its current list of game properties by using the command line option without any
+parameter. Here's the current output you get at the time I write those lines:
+
+        Game properties:
+        * et:
+           - protocols: 72, 80, 83, 84
+           - options: send-empty-servers, send-full-servers
+           - heartbeats: EnemyTerritory-1 (alive), ETFlatline-1 (dead)
+
+        * wolfmp:
+           - protocols: 50, 59, 60
+           - options: none
+           - heartbeats: Wolfenstein-1 (alive), WolfFlatline-1 (dead)
+
+        * Quake3Arena:
+           - protocols: 66, 67, 68
+           - options: none
+           - heartbeats: QuakeArena-1 (alive)
+
+"et", "wolfmp" and "Quake3Arena" are the respective game names for WoET, RtCW
+and Q3A. Each of them have been assigned several protocol numbers, options, and
+up to 2 heartbeat tags (one for alive servers, one for dying servers). All these
+values are optional: a game name can have no protocol, no option and no tag
+associated to it, although there would be no point to that.
+
+Normal (alive) heartbeat tags are used to figure out the game name when servers
+don't send it, like those of Q3A and some old Wolfenstein versions. Dead
+heartbeat tags are simply ignored, they don't trigger the sending of a "getinfo"
+message, unlike normal heartbeats.
+
+Protocol numbers are used to figure out the game name when clients don't send it
+with their "getservers" requests, and unfortunately this is the case for all the
+anonymous games currently supported. If the protocol declared by the client
+doesn't match any of the registered protocol numbers, dpmaster will use the
+first server of an anonymous game it will find, that uses this very protocol
+number, as the reference for the name. In other words, it will handle the query
+as if it has declared the same game name as this server.
+
+Options allows you to specify non-standard behaviours for a game. For example,
+the WoET's clients expect the master server to send them the complete list of
+servers, even though they don't specify that they want empty and full servers,
+like other Q3A-derived games do. By associating the proper options to its game
+name ("et"), we make sure that dpmaster will send the expected list anyway.
+The available options are: "send-empty-servers" and "send-full-servers".
+
+In order to modify the properties of a game, you have to use the command line
+option, with the game name as the first parameter, and then the modifications
+you want. You can either assign new values to a property (using "="), add values
+to it (using "+="), or remove values from it (using "-="). The values in the
+list must be separated by commas. No spaces are allowed, neither in the game
+name, nor in the list of modifications. The available properties are:
+"protocols", "options", "heartbeat" (normal heartbeat), and "flatline" (dying
+heartbeat).
+
+And you can have multiple game property changes in your command line, obviously.
+Here are a few examples.
+
+To add protocol 70 and a dead heartbeat to Q3A:
+
+        dpmaster -g Quake3Arena protocols+=70 flatline=Q3ADeadHB
+
+To remove all protocols from RtCW and give it 2 brand new ones, 4321 and 1234:
+
+        dpmaster -g wolfmp protocols=4321,1234
+
+To not send full servers to WoET clients, and to remove protocol 50 from RtCW:
+
+        dpmaster -g et options-=send-full-servers -g wolfmp protocols-=50
+
+The game properties has been added to dpmaster in order to support anonymous
+games, but it can also be useful for other games. For instance, you can force
+dpmaster to send empty servers to Warsow clients like this:
+
+        dpmaster -g Warsow options=send-empty-servers
+
+You could also specify a list of protocol numbers here, but since Warsow uses
+the DarkPlaces master protocol, both its clients and servers declares their game
+names, so it would be useless.
+
+Note that you can ask for the list of properties after you have declared some
+modifications, using a final "-g" on the command line. In this case, the printed
+list will contain your modifications. It's a good way to check that you didn't
+make any mistake before actually running your master server.
+
+
+9) ADDRESS MAPPING:
+
 Address mapping allows you to tell dpmaster to transmit an IPv4 address instead
 of another one to the clients, in the "getserversResponse" messages. It can be
 useful in several cases. Imagine for instance that you have a dpmaster and a
@@ -283,7 +391,7 @@
 test purposes - do NOT run your master with this option!).
 
 
-9) LISTENING INTERFACES:
+10) LISTENING INTERFACES:
 
 By default, dpmaster creates one IPv4 socket and one IPv6 socket (if IPv6
 support is available of course). It will listen on every network interface, on
@@ -320,8 +428,15 @@
 indices in the Wikipedia article about IPv6 <http://en.wikipedia.org/wiki/IPv6>.
 
 
-10) VERSION HISTORY:
+11) VERSION HISTORY:
 
+    - version 2.2-dev:
+        New system for managing game properties (see GAME PROPERTIES above)
+        Support for RtCW and WoET, using the game properties
+        Shutdown heartbeats and unknown heartbeats are now ignored
+        The chroot jail was preventing daemonization (fixed thanks to LordHavoc)
+        The game type was incorrect when printing the server list in the log
+
     - version 2.1:
         A gametype value can now be any string, not just a number
 
@@ -421,7 +536,7 @@
         First publicly available version
 
 
-11) CONTACTS & LINKS:
+12) CONTACTS & LINKS:
 
 You can get the latest versions of DarkPlaces and dpmaster on the DarkPlaces
 home page <http://icculus.org/twilight/darkplaces/>.

Modified: trunk/dpmaster/src/common.h
===================================================================
--- trunk/dpmaster/src/common.h	2010-08-22 16:52:33 UTC (rev 10413)
+++ trunk/dpmaster/src/common.h	2010-08-22 17:06:16 UTC (rev 10414)
@@ -3,7 +3,7 @@
 
 	Common header file for dpmaster
 
-	Copyright (C) 2004-2009  Mathieu Olivier
+	Copyright (C) 2004-2010  Mathieu Olivier
 
 	This program is free software; you can redistribute it and/or modify
 	it under the terms of the GNU General Public License as published by
@@ -27,6 +27,7 @@
 
 #include <assert.h>
 #include <errno.h>
+#include <fcntl.h>
 #include <limits.h>
 #include <signal.h>
 #include <stdarg.h>
@@ -77,6 +78,7 @@
 {
 	CMDLINE_STATUS_OK,
 	CMDLINE_STATUS_SHOW_HELP,
+	CMDLINE_STATUS_SHOW_GAME_PROPERTIES,
 
 	// Errors
 	CMDLINE_STATUS_INVALID_OPT,

Modified: trunk/dpmaster/src/dpmaster.c
===================================================================
--- trunk/dpmaster/src/dpmaster.c	2010-08-22 16:52:33 UTC (rev 10413)
+++ trunk/dpmaster/src/dpmaster.c	2010-08-22 17:06:16 UTC (rev 10414)
@@ -4,7 +4,7 @@
 
 	An open master server
 
-	Copyright (C) 2002-2009  Mathieu Olivier
+	Copyright (C) 2002-2010  Mathieu Olivier
 
 	This program is free software; you can redistribute it and/or modify
 	it under the terms of the GNU General Public License as published by
@@ -32,7 +32,7 @@
 // ---------- Constants ---------- //
 
 // Version of dpmaster
-#define VERSION "2.1"
+#define VERSION "2.2-dev"
 
 
 // ---------- Private variables ---------- //
@@ -51,12 +51,21 @@
 		0
 	},
 	{
+		"game-properties",
+		"[game_name <property> ...]",
+		"Without parameter, print the list of all known games and their properties.\n"
+		"   Otherwise, update the properties of a game according to the other parameters.",
+		{ 0, 0 },
+		'g',
+		0,
+		UINT_MAX
+	},
+	{
 		"game-policy",
 		"<accept|reject> <game_name> ...",
 		"Accept or reject the listed games. Can be specified more than once only\n"
 		"   if all instances set the same policy (\"accept\" or \"reject\").\n"
-		"   All non-listed games will implicitely get the opposite policy."
-		,
+		"   All non-listed games will implicitely get the opposite policy.",
 		{ 0, 0 },
 		'\0',
 		2,
@@ -276,6 +285,17 @@
 	if (strcmp (opt_name, "allow-loopback") == 0)
 		allow_loopback = true;
 
+	// Game properties
+	else if (strcmp (opt_name, "game-properties") == 0)
+	{
+		if (nb_params == 0)
+			return CMDLINE_STATUS_SHOW_GAME_PROPERTIES;
+		else if (nb_params == 1)
+			return CMDLINE_STATUS_NOT_ENOUGH_OPT_PARAMS;
+		else
+			return Game_UpdateProperties (params[0], &params[1], nb_params - 1);
+	}
+
 	// Game policy
 	else if (strcmp (opt_name, "game-policy") == 0)
 		return Game_DeclarePolicy (params[0], &params[1], nb_params - 1);
@@ -670,7 +690,8 @@
 				break;
 
 			default:
-				assert(cmdline_status == CMDLINE_STATUS_SHOW_HELP);
+				assert (cmdline_status == CMDLINE_STATUS_SHOW_HELP ||
+						cmdline_status == CMDLINE_STATUS_SHOW_GAME_PROPERTIES);
 				errormsg_part1 = NULL;
 				errormsg_part2 = NULL;
 				break;
@@ -805,6 +826,10 @@
 {
 	cmdline_status_t valid_options;
 
+	// Game properties must be initialized first, since the user
+	// may modify them using the command line's arguments
+	Game_InitProperties ();
+
 	// Get the options from the command line
 	valid_options = ParseCommandLine (argc, argv);
 
@@ -813,8 +838,21 @@
 	// If something goes wrong with the command line, exit
 	if (valid_options != CMDLINE_STATUS_OK)
 	{
-		if (valid_options == CMDLINE_STATUS_SHOW_HELP)
-			PrintHelp ();
+		switch (valid_options)
+		{
+			case CMDLINE_STATUS_SHOW_HELP:
+				PrintHelp ();
+				break;
+
+			case CMDLINE_STATUS_SHOW_GAME_PROPERTIES:
+				Game_PrintProperties ();
+				break;
+			
+			default:
+				// Nothing
+				break;
+		}
+
 		return EXIT_FAILURE;
 	}
 
@@ -941,7 +979,7 @@
 							peer_address, nb_bytes);
 				continue;
 			}
-			if (*((unsigned int*)packet) != 0xFFFFFFFF)
+			if (packet[0] != '\xFF' || packet[1] != '\xFF' || packet[2] != '\xFF' || packet[3] != '\xFF')
 			{
 				Com_Printf (MSG_WARNING,
 							"> WARNING: rejected packet from %s (invalid header)\n",

Modified: trunk/dpmaster/src/games.c
===================================================================
--- trunk/dpmaster/src/games.c	2010-08-22 16:52:33 UTC (rev 10413)
+++ trunk/dpmaster/src/games.c	2010-08-22 17:06:16 UTC (rev 10414)
@@ -3,7 +3,7 @@
 
 	Games management for dpmaster
 
-	Copyright (C) 2009  Mathieu Olivier
+	Copyright (C) 2009-2010  Mathieu Olivier
 
 	This program is free software; you can redistribute it and/or modify
 	it under the terms of the GNU General Public License as published by
@@ -78,7 +78,7 @@
 }
 
 
-// ---------- Public functions ---------- //
+// ---------- Public functions (game policy) ---------- //
 
 /*
 ====================
@@ -144,3 +144,671 @@
 {
 	return (Game_Find (game_name, NULL) ^ reject_when_known);
 }
+
+
+// ---------- Private constants ---------- //
+
+// Gamenames
+#define GAMENAME_Q3A	"Quake3Arena"	// Quake 3 Arena
+#define GAMENAME_RTCW	"wolfmp"		// Return to Castle Wolfenstein
+#define GAMENAME_WOET	"et"			// Wolfenstein: Enemy Territory
+
+
+// ---------- Private types (game properties) ---------- //
+
+typedef struct
+{
+	game_options_t	value;
+	const char*		string;
+} game_option_string_t;
+
+typedef struct
+{
+	int					protocol;
+	game_properties_t*	game;
+} game_protocol_assoc_t;
+
+
+// ---------- Private variables (game properties) ---------- //
+
+static const game_option_string_t option_strings [] =
+{
+	{ GAME_OPTION_SEND_EMPTY_SERVERS,	"send-empty-servers"	},
+	{ GAME_OPTION_SEND_FULL_SERVERS,	"send-full-servers"		},
+
+	{ GAME_OPTION_NONE, NULL },		// Marks the end of the list
+};
+
+static game_properties_t* game_properties_list = NULL;
+
+static game_protocol_assoc_t* game_protocols = NULL;	// TODO: sort it to allow binary searches
+static unsigned int nb_game_protocols = 0;
+
+
+// ---------- Private functions (game properties) ---------- //
+
+/*
+====================
+Game_GetAnonymous
+
+Returns the properties of an anonymous game
+====================
+*/
+static game_properties_t* Game_GetAnonymous (const char* game, qboolean creation_allowed)
+{
+	game_properties_t* props = game_properties_list;
+	
+	while (props != NULL)
+	{
+		if (strcmp (game, props->name) == 0)
+			return props;
+		
+		props = props->next;
+	}
+
+	if (creation_allowed)
+	{
+		// Empty game names, or game names containing spaces aren't allowed
+		if (game[0] != '\0' && strchr (game, ' ') == NULL)
+		{
+			props = malloc (sizeof (*props));
+			if (props != NULL)
+			{
+				memset (props, 0, sizeof (*props));
+				props->name = game;
+				
+				props->next = game_properties_list;
+				game_properties_list = props;
+		
+				return props;
+			}
+		}
+	}
+
+	return NULL;
+}
+
+
+/*
+====================
+Game_FindAnonymous
+ 
+Find game properties in the list of game protocols
+After the call, *index_ptr will contain the index where the game is stored in game_names (or should be stored, if it is not present)
+====================
+*/
+static qboolean Game_FindAnonymous (int protocol, unsigned int* index_ptr)
+{
+	int left = 0;
+
+	if (game_protocols != NULL)
+	{
+		int right = nb_game_protocols - 1;
+
+		while (left <= right)
+		{
+			int middle;
+
+			middle = (left + right) / 2;
+
+			if (game_protocols[middle].protocol == protocol)
+			{
+				if (index_ptr != NULL)
+					*index_ptr = middle;
+				return true;
+			}
+
+			if (game_protocols[middle].protocol > protocol)
+				right = middle - 1;
+			else
+				left = middle + 1;
+		}
+	}
+		
+	if (index_ptr != NULL)
+		*index_ptr = left;
+	return false;
+}
+
+
+/*
+====================
+Game_RemoveAllProtocols
+
+Remove all the protocol numbers associated to a game
+====================
+*/
+static void Game_RemoveAllProtocols (game_properties_t* game_props)
+{
+	unsigned int proto_ind, proto_copy_ind;
+
+	proto_copy_ind = 0;
+	for (proto_ind = 0; proto_ind < nb_game_protocols; proto_ind++)
+	{
+		if (game_protocols[proto_ind].game != game_props)
+		{
+			if (proto_ind != proto_copy_ind)
+				memcpy (&game_protocols[proto_copy_ind], &game_protocols[proto_ind], sizeof (game_protocols[proto_copy_ind]));
+			proto_copy_ind++;
+		}
+	}
+
+	nb_game_protocols = proto_copy_ind;
+}
+
+
+/*
+====================
+Game_AddProtocol
+
+Add a protocol number to the properties of a game
+====================
+*/
+static cmdline_status_t Game_AddProtocol (game_properties_t* game_props, int protocol)
+{
+	unsigned int index;
+	
+	if (! Game_FindAnonymous (protocol, &index))
+	{
+		game_protocol_assoc_t* new_array;
+
+		new_array = realloc (game_protocols, (nb_game_protocols + 1) * sizeof (game_protocols[0]));
+		if (new_array == NULL)
+			return CMDLINE_STATUS_NOT_ENOUGH_MEMORY;
+		game_protocols = new_array;
+
+		memmove(&game_protocols[index + 1], &game_protocols[index], (nb_game_protocols - index) * sizeof (game_protocols[0]));
+		game_protocols[index].protocol = protocol;
+		nb_game_protocols++;
+	}
+
+	game_protocols[index].game = game_props;
+	return CMDLINE_STATUS_OK;
+}
+
+
+/*
+====================
+Game_RemoveProtocol
+
+Remove a protocol number from the properties of a game
+====================
+*/
+static cmdline_status_t Game_RemoveProtocol (game_properties_t* game_props, int protocol)
+{
+	unsigned int index;
+	
+	// FIXME? shouldn't we abort if the protocol number isn't used?
+	if (Game_FindAnonymous (protocol, &index))
+	{
+		if (game_protocols[index].game != game_props)
+			return CMDLINE_STATUS_INVALID_OPT_PARAMS;
+
+		memmove(&game_protocols[index], &game_protocols[index + 1], (nb_game_protocols - index - 1) * sizeof (game_protocols[0]));
+		nb_game_protocols--;
+	}
+	
+	return CMDLINE_STATUS_OK;
+}
+
+
+/*
+====================
+Game_RemoveHeartbeat
+
+Remove an heartbeat tag from the properties of a game
+====================
+*/
+static void Game_RemoveHeartbeat (game_properties_t* game_props, heartbeat_type_t hb_type)
+{
+	char* tag = game_props->heartbeats[hb_type];
+
+	if (tag != NULL)
+	{
+		free (tag);
+		game_props->heartbeats[hb_type] = NULL;
+	}
+}
+
+
+/*
+====================
+Game_UpdateHeartbeat
+
+Add or remove an heartbeat to the properties of a game
+====================
+*/
+static cmdline_status_t Game_UpdateHeartbeat (game_properties_t* game_props, heartbeat_type_t hb_type, const char* value, qboolean remove)
+{
+	if (remove)
+	{
+		if (strcmp (game_props->heartbeats[hb_type], value) == 0)
+			Game_RemoveHeartbeat (game_props, hb_type);
+	}
+	else
+	{
+		// The tag used by the DarkPlaces protocol is reserved
+		if (strcmp (value, HEARTBEAT_DARKPLACES) == 0)
+			return CMDLINE_STATUS_INVALID_OPT_PARAMS;
+
+		// You can't have more than one heartbeat tag for each type
+		if (game_props->heartbeats[hb_type] != NULL)
+			return CMDLINE_STATUS_INVALID_OPT_PARAMS;
+
+		game_props->heartbeats[hb_type] = strdup (value);
+		if (game_props->heartbeats[hb_type] == NULL)
+			return CMDLINE_STATUS_NOT_ENOUGH_MEMORY;
+	}
+
+	return CMDLINE_STATUS_OK;
+}
+
+
+/*
+====================
+Game_UpdateOption
+
+Add or remove an option to the properties of a game
+====================
+*/
+static cmdline_status_t Game_UpdateOption (game_properties_t* game_props, const char* option_name, qboolean remove)
+{
+	size_t option_ind;
+
+	for (option_ind = 0; option_strings[option_ind].value != GAME_OPTION_NONE; option_ind++)
+	{
+		const game_option_string_t* option = &option_strings[option_ind];
+
+		if (strcmp(option->string, option_name) == 0)
+		{
+			if (remove)
+				game_props->options &= ~(option->value);
+			else
+				game_props->options |= option->value;
+			return CMDLINE_STATUS_OK;
+		}
+	}
+
+	return CMDLINE_STATUS_INVALID_OPT_PARAMS;
+}
+
+
+/*
+====================
+Game_UpdateProperty
+
+Update a game property
+====================
+*/
+static cmdline_status_t Game_UpdateProperty (game_properties_t* game_props, const char* prop_name, char* prop_value,
+											 qboolean reset_property, qboolean remove_values)
+{
+	char* next_value;
+
+	// No property accepts values with a blank space in it so far
+	if (strchr (prop_value, ' ') != NULL)
+		return CMDLINE_STATUS_INVALID_OPT_PARAMS;
+
+	if (reset_property)
+	{
+		if (strcmp (prop_name, "protocols") == 0)
+		{
+			Game_RemoveAllProtocols (game_props);
+		}
+		else if (strcmp (prop_name, "options") == 0)
+		{
+			game_props->options = GAME_OPTION_NONE;
+		}
+		else if (strcmp (prop_name, "heartbeat") == 0)
+		{
+			Game_RemoveHeartbeat (game_props, HEARTBEAT_TYPE_ALIVE);
+		}
+		else if (strcmp (prop_name, "flatline") == 0)
+		{
+			Game_RemoveHeartbeat (game_props, HEARTBEAT_TYPE_DEAD);
+		}
+		else
+			return CMDLINE_STATUS_INVALID_OPT_PARAMS;
+	}
+	
+	next_value = strtok (prop_value, ",");
+	while (next_value != NULL)
+	{
+		cmdline_status_t result;
+
+		if (strcmp (prop_name, "protocols") == 0)
+		{
+			char* end_ptr;
+
+			int protocol = (int)strtol (next_value, &end_ptr, 0);
+			if (*end_ptr != '\0')
+				return CMDLINE_STATUS_INVALID_OPT_PARAMS;
+			
+			if (remove_values)
+				result = Game_RemoveProtocol (game_props, protocol);
+			else
+				result = Game_AddProtocol (game_props, protocol);
+			if (result != CMDLINE_STATUS_OK)
+				return result;
+		}
+		else if (strcmp (prop_name, "options") == 0)
+		{
+			result = Game_UpdateOption (game_props, next_value, remove_values);
+			if (result != CMDLINE_STATUS_OK)
+				return result;
+		}
+		else if (strcmp (prop_name, "heartbeat") == 0)
+		{
+			result = Game_UpdateHeartbeat (game_props, HEARTBEAT_TYPE_ALIVE, next_value, remove_values);
+			if (result != CMDLINE_STATUS_OK)
+				return result;
+		}
+		else if (strcmp (prop_name, "flatline") == 0)
+		{
+			result = Game_UpdateHeartbeat (game_props, HEARTBEAT_TYPE_DEAD, next_value, remove_values);
+			if (result != CMDLINE_STATUS_OK)
+				return result;
+		}
+		else
+			return CMDLINE_STATUS_INVALID_OPT_PARAMS;
+
+		next_value = strtok (NULL, ",");
+	}
+
+	return CMDLINE_STATUS_OK;
+}
+
+
+// ---------- Public functions (game properties) ---------- //
+
+/*
+====================
+Game_InitProperties
+
+Initialize the game properties using a built-in list
+====================
+*/
+void Game_InitProperties (void)
+{
+	typedef struct
+	{
+		const char*		gamename;
+		size_t			nb_props;
+		const char*		props [4];
+	} builtin_props_t;
+
+	builtin_props_t builtin_props_array [] =
+	{
+		// Quake 3 Arena
+		{
+			GAMENAME_Q3A,
+			2,
+			{
+				"protocols=66,67,68",
+				"heartbeat=QuakeArena-1",
+			},
+		},
+
+		// Return to Castle Wolfenstein
+		{
+			GAMENAME_RTCW,
+			3,
+			{
+				"protocols=50,59,60",
+				"heartbeat=Wolfenstein-1",
+				"flatline=WolfFlatline-1",
+			},
+		},
+
+		// Wolfenstein: Enemy Territory
+		{
+			GAMENAME_WOET,
+			4,
+			{
+				"protocols=72,80,83,84",
+				"options=send-empty-servers,send-full-servers",
+				"heartbeat=EnemyTerritory-1",
+				"flatline=ETFlatline-1",
+			},
+		},
+
+	};
+
+	size_t game_count = sizeof (builtin_props_array) / sizeof (builtin_props_array[0]);
+	size_t game_ind;
+
+	for (game_ind = 0; game_ind < game_count; game_ind++)
+	{
+		builtin_props_t* builtin_props = &builtin_props_array[game_ind];
+		Game_UpdateProperties (builtin_props->gamename, builtin_props->props, builtin_props->nb_props);
+	}
+}
+
+
+/*
+====================
+Game_PrintProperties
+
+Print the list of known game properties
+====================
+*/
+void Game_PrintProperties (void)
+{
+	const game_properties_t* props;
+	const char* hb_types [] = { "alive", "dead" };
+	
+	Com_Printf (MSG_ERROR, "\nGame properties:\n");
+
+	props = game_properties_list;
+	while (props != NULL)
+	{
+		unsigned int count, ind;
+		
+		// Name
+		Com_Printf (MSG_ERROR, "* %s:\n", props->name);
+
+		// Protocols
+		Com_Printf (MSG_ERROR, "   - protocols:");
+		count = 0;
+		for (ind = 0; ind < nb_game_protocols; ind++)
+		{
+			const game_protocol_assoc_t* assoc = &game_protocols[ind];
+
+			if (assoc->game == props)
+			{
+				Com_Printf (MSG_ERROR, "%s %d", count > 0 ? "," : "",
+							assoc->protocol);
+				count++;
+			}
+		}
+		if (count == 0)
+			Com_Printf (MSG_ERROR, " none");
+		Com_Printf (MSG_ERROR, "\n");
+		
+		// Options
+		Com_Printf (MSG_ERROR, "   - options:");
+		count = 0;
+		if (props->options != GAME_OPTION_NONE)
+		{
+			for (ind = 0; option_strings[ind].value != GAME_OPTION_NONE; ind++)
+			{
+				const game_option_string_t* option = &option_strings[ind];
+
+				if ((props->options & option->value) != 0)
+				{
+					Com_Printf (MSG_ERROR, "%s %s", count > 0 ? "," : "",
+								option->string);
+					count++;
+				}
+			}
+		}
+		else
+			Com_Printf (MSG_ERROR, " none");
+		Com_Printf (MSG_ERROR, "\n");
+		
+		// Heartbeats
+		Com_Printf (MSG_ERROR, "   - heartbeats:");
+		count = 0;
+		for (ind = 0; ind < sizeof (props->heartbeats) / sizeof (props->heartbeats[0]); ind++)
+		{
+			const char* tag = props->heartbeats[ind];
+
+			if (tag != NULL)
+			{
+				Com_Printf (MSG_ERROR, "%s %s (%s)", count > 0 ? "," : "",
+							tag, hb_types[ind]);
+				count++;
+			}
+		}
+		if (count == 0)
+			Com_Printf (MSG_ERROR, " none");
+		Com_Printf (MSG_ERROR, "\n");
+
+		Com_Printf (MSG_ERROR, "\n");
+		props = props->next;
+	}
+}
+
+
+/*
+====================
+Game_UpdateProperties
+
+Update the properties of a game according to the given list of properties
+====================
+*/
+cmdline_status_t Game_UpdateProperties (const char* game, const char** props, size_t nb_props)
+{
+	unsigned int prop_ind;
+	game_properties_t* game_props = Game_GetAnonymous (game, true);
+	
+	if (game_props == NULL)
+		return CMDLINE_STATUS_NOT_ENOUGH_MEMORY;
+
+	// Parse the properties and apply them
+	for (prop_ind = 0; prop_ind < nb_props; prop_ind++)
+	{
+		char* work_buff = strdup (props[prop_ind]);
+		char* equal_sign;
+		
+		if (work_buff == NULL)
+			return CMDLINE_STATUS_NOT_ENOUGH_MEMORY;
+		
+		equal_sign = strchr (work_buff, '=');
+		if (equal_sign != NULL && equal_sign != work_buff)
+		{
+			cmdline_status_t result;
+			qboolean reset_property, remove_values;
+
+			if (equal_sign[-1] == '+')
+			{
+				equal_sign[-1] = '\0';
+				reset_property = false;
+				remove_values = false;
+			}
+			else if (equal_sign[-1] == '-')
+			{
+				equal_sign[-1] = '\0';
+				reset_property = false;
+				remove_values = true;
+			}
+			else
+			{
+				equal_sign[0] = '\0';
+				reset_property = true;
+				remove_values = false;
+			}
+			
+			result = Game_UpdateProperty (game_props, work_buff, equal_sign + 1, reset_property, remove_values);
+			free(work_buff);
+
+			if (result != CMDLINE_STATUS_OK)
+				return result;
+			else
+				continue;
+		}
+		
+		free (work_buff);
+		return CMDLINE_STATUS_INVALID_OPT_PARAMS;
+	}
+
+	return CMDLINE_STATUS_OK;
+}
+
+
+/*
+====================
+Game_GetNameByProtocol
+
+Returns the name of a game based on its protocol number
+====================
+*/
+const char* Game_GetNameByProtocol (int protocol, game_options_t* options)
+{
+	const game_properties_t* props;
+	unsigned int index;
+
+	if (Game_FindAnonymous (protocol, &index))
+	{
+		props = game_protocols[index].game;
+		
+		if (options != NULL)
+			*options = props->options;
+		return props->name;
+	}
+
+	return NULL;
+}
+
+
+/*
+====================
+Game_GetPropertiesByHeartbeat
+
+Returns the properties of the game which uses this heartbeat tag.
+"flatline_heartbeat" will be set to "true" if it's a flatline tag
+====================
+*/
+const game_properties_t* Game_GetPropertiesByHeartbeat (const char* heartbeat_tag, qboolean* flatline_heartbeat)
+{
+	game_properties_t* props = game_properties_list;
+	
+	while (props != NULL)
+	{
+		size_t hb_ind;
+
+		for (hb_ind = 0; hb_ind < NB_HEARTBEAT_TYPES; hb_ind++)
+		{
+			const char* tag = props->heartbeats[hb_ind];
+
+			if (tag != NULL && strcmp (heartbeat_tag, tag) == 0)
+			{
+				*flatline_heartbeat = (hb_ind == HEARTBEAT_TYPE_DEAD);
+				return props;
+			}
+		}
+		
+		props = props->next;
+	}
+
+	*flatline_heartbeat = false;
+	return NULL;
+}
+
+
+/*
+====================
+Game_GetOptions
+
+Returns the options of a game
+====================
+*/
+game_options_t Game_GetOptions (const char* game)
+{
+	const game_properties_t* props = Game_GetAnonymous (game, false);
+
+	if (props != NULL)
+		return props->options;
+	else
+		return GAME_OPTION_NONE;
+}

Modified: trunk/dpmaster/src/games.h
===================================================================
--- trunk/dpmaster/src/games.h	2010-08-22 16:52:33 UTC (rev 10413)
+++ trunk/dpmaster/src/games.h	2010-08-22 17:06:16 UTC (rev 10414)
@@ -3,7 +3,7 @@
 
 	Games management for dpmaster
 
-	Copyright (C) 2009  Mathieu Olivier
+	Copyright (C) 2009-2010  Mathieu Olivier
 
 	This program is free software; you can redistribute it and/or modify
 	it under the terms of the GNU General Public License as published by
@@ -25,7 +25,7 @@
 #define _GAMES_H_
 
 
-// ---------- Public functions ---------- //
+// ---------- Public functions (game policy) ---------- //
 
 // Declare the server policy regarding which games are allowed on this master
 cmdline_status_t Game_DeclarePolicy (const char* policy, const char** games, unsigned int nb_games);
@@ -34,4 +34,65 @@
 qboolean Game_IsAccepted (const char* game_name);
 
 
+// ---------- Public constants (game properties) ---------- //
+
+// Heartbeat tag for the DarkPlaces protocol
+#define HEARTBEAT_DARKPLACES	"DarkPlaces"
+
+
+// ---------- Public types (game properties) ---------- //
+
+typedef enum
+{
+	GAME_OPTION_NONE				= 0,
+	
+	// Send empty servers even when the "getservers" requests don't ask for them
+	GAME_OPTION_SEND_EMPTY_SERVERS	= (1 << 0),
+	
+	// Send full servers even when the "getservers" requests don't ask for them
+	GAME_OPTION_SEND_FULL_SERVERS	= (1 << 1),
+} game_options_t;
+
+typedef enum
+{
+	HEARTBEAT_TYPE_ALIVE,
+	HEARTBEAT_TYPE_DEAD,
+	
+	NB_HEARTBEAT_TYPES,
+} heartbeat_type_t;
+
+typedef struct game_properties_s
+{
+	const char*					name;
+	game_options_t				options;
+	char*						heartbeats [NB_HEARTBEAT_TYPES];	// Heartbeat tags
+	struct game_properties_s*	next;
+} game_properties_t;
+
+
+// ---------- Public functions (game properties) ---------- //
+
+// Initialize the game properties using a built-in list
+void Game_InitProperties (void);
+
+// Print the list of known game properties
+void Game_PrintProperties (void);
+
+// Update the properties of a game according to the given list of properties
+cmdline_status_t Game_UpdateProperties (const char* game, const char** props, size_t nb_props);
+
+// Set the name that is returned when an anonymous game uses an unknown protocol number
+cmdline_status_t Game_SetDefaultAnonymous (const char* game);
+
+// Returns the name of a game based on its protocol number
+const char* Game_GetNameByProtocol (int protocol, game_options_t* options);
+
+// Returns the properties of the game which uses this heartbeat tag.
+// "flatline_heartbeat" will be set to "true" if it's a flatline tag
+const game_properties_t* Game_GetPropertiesByHeartbeat (const char* heartbeat_tag, qboolean* flatline_heartbeat);
+
+// Returns the options of a game
+game_options_t Game_GetOptions (const char* game);
+
+
 #endif  // #ifndef _GAMES_H_

Modified: trunk/dpmaster/src/messages.c
===================================================================
--- trunk/dpmaster/src/messages.c	2010-08-22 16:52:33 UTC (rev 10413)
+++ trunk/dpmaster/src/messages.c	2010-08-22 17:06:16 UTC (rev 10414)
@@ -3,7 +3,7 @@
 
 	Message management for dpmaster
 
-	Copyright (C) 2004-2009  Mathieu Olivier
+	Copyright (C) 2004-2010  Mathieu Olivier
 
 	This program is free software; you can redistribute it and/or modify
 	it under the terms of the GNU General Public License as published by
@@ -36,9 +36,6 @@
 // Period of validity for a challenge string (in secondes)
 #define TIMEOUT_CHALLENGE 2
 
-// Gamename used for Q3A
-#define GAMENAME_Q3A "Quake3Arena"
-
 // Maximum size of a reponse packet
 #define MAX_PACKET_SIZE_OUT 1400
 
@@ -47,7 +44,7 @@
 
 // Q3: "heartbeat QuakeArena-1\x0A"
 // DP: "heartbeat DarkPlaces\x0A"
-#define S2M_HEARTBEAT "heartbeat"
+#define S2M_HEARTBEAT "heartbeat "
 
 // Q3 & DP & QFusion: "getinfo A_Challenge"
 #define M2S_GETINFO "getinfo"
@@ -209,12 +206,12 @@
 Send a "getinfo" message to a server
 ====================
 */
-static void SendGetInfo (server_t* server, socket_t recv_socket)
+static void SendGetInfo (server_t* server, socket_t recv_socket, qboolean force_new_challenge)
 {
 	char msg [64] = "\xFF\xFF\xFF\xFF" M2S_GETINFO " ";
 	size_t msglen;
 
-	if (!server->challenge_timeout || server->challenge_timeout < crt_time)
+	if (force_new_challenge || !server->challenge_timeout || server->challenge_timeout < crt_time)
 	{
 		const char* challenge;
 
@@ -239,6 +236,75 @@
 
 /*
 ====================
+HandleHeartbeat
+
+Parse heartbeat requests
+====================
+*/
+static void HandleHeartbeat (const char* msg, const struct sockaddr_storage* addr, socklen_t addrlen, socket_t recv_socket)
+{
+	char tag [64];
+	const game_properties_t* game_props;
+	server_t* server;
+	qboolean flatlineHeartbeat;
+
+	// Extract the tag
+	sscanf (msg, "%63s", tag);
+	Com_Printf (MSG_NORMAL, "> %s ---> heartbeat (%s)\n",
+				peer_address, tag);
+
+	// If it's not a game that uses the DarkPlaces protocol
+	if (strcmp (tag, HEARTBEAT_DARKPLACES) != 0)
+	{
+		game_props = Game_GetPropertiesByHeartbeat (tag, &flatlineHeartbeat);
+		if (game_props == NULL)
+		{
+			Com_Printf (MSG_WARNING,
+						"> WARNING: Rejecting heartbeat from %s (heartbeat \"%s\" is unknown)\n",
+						peer_address, tag);
+			return;
+		}
+
+		Com_Printf (MSG_DEBUG, "  - belongs to game \"%s\"\n",
+					game_props->name);
+
+		// Ignore flatline (shutdown) heartbeats
+		if (flatlineHeartbeat)
+		{
+			Com_Printf (MSG_NORMAL, "  - flatline heartbeat (ignored)\n");
+			return;
+		}
+
+		// If the game isn't accepted on this server, ignore the heartbeat
+		if (! Game_IsAccepted (game_props->name))
+		{
+			Com_Printf (MSG_WARNING,
+						"> WARNING: Rejecting heartbeat from %s (game \"%s\" is not accepted)\n",
+						peer_address, game_props->name);
+			return;
+		}
+	}
+	else
+		game_props = NULL;
+
+	// Get the server in the list (add it to the list if necessary)
+	server = Sv_GetByAddr (addr, addrlen, true);
+	if (server == NULL)
+		return;
+
+	assert (server->state != sv_state_unused_slot);
+
+	// Ask for some infos.
+	// Force a new challenge if the heartbeat tag has changed
+	SendGetInfo (server, recv_socket, server->hb_properties != game_props);
+
+	// Save the game properties for a future use
+	server->hb_properties = game_props;
+}
+
+
+/*
+====================
 HandleGetServers
 
 Parse getservers requests and send the appropriate response
@@ -255,6 +321,7 @@
 	size_t packetind;
 	server_t* sv;
 	int protocol;
+	game_options_t game_options = GAME_OPTION_NONE;
 	char gametype [GAMETYPE_LENGTH] = "0";
 	qboolean use_dp_protocol;
 	qboolean opt_empty = false;
@@ -306,6 +373,8 @@
 		if (space)
 			*space = '\0';
 		msg_ptr = msg_ptr + strlen (gamename);
+		
+		game_options = Game_GetOptions (gamename);
 
 		// Read the protocol number
 		protocol = (int)strtol (msg_ptr, &end_ptr, 0);
@@ -317,24 +386,40 @@
 			return;
 		}
 	}
-	// Else, it comes from a Quake III Arena client
+	// Else, it comes from an anonymous client
 	else
 	{
-		strncpy (gamename, GAMENAME_Q3A, sizeof (gamename) - 1);
-		gamename[sizeof (gamename) - 1] = '\0';
+		const char* anon_game = Game_GetNameByProtocol (protocol, &game_options);
+
+		// If we can't determine the game name from the protocol, we will just use
+		// the 1st server we found with this protocol to get a game name
+		if (anon_game != NULL)
+		{
+			strncpy (gamename, anon_game, sizeof (gamename) - 1);
+			gamename[sizeof (gamename) - 1] = '\0';
+		}
+		else
+			gamename[0] = '\0';
+
 		msg_ptr = end_ptr;
 	}
 
-	Com_Printf (MSG_NORMAL, "> %s ---> %s (%s)\n", peer_address, request_name,
-				gamename);
+	Com_Printf (MSG_NORMAL, "> %s ---> %s (%s, %i)\n", peer_address, request_name,
+				gamename[0] != '\0' ? gamename : "unknown game", protocol);
 
-	if (! Game_IsAccepted (gamename))
+	if (gamename[0] != '\0' && ! Game_IsAccepted (gamename))
 	{
 		Com_Printf (MSG_WARNING,
 					"> WARNING: Rejecting %s from %s (game \"%s\" is not accepted)\n",
 					request_name, peer_address, gamename);
 		return;
 	}
+	
+	// Apply the game options
+	if ((game_options & GAME_OPTION_SEND_EMPTY_SERVERS) != 0)
+		opt_empty = true;
+	if ((game_options & GAME_OPTION_SEND_FULL_SERVERS) != 0)
+		opt_full = true;
 
 	// Parse the filtering options
 	strncpy (filter_options, msg_ptr, sizeof (filter_options) - 1);
@@ -439,16 +524,42 @@
 				Com_Printf (MSG_DEBUG,
 							"    Reject: gametype \"%s\" != requested \"%s\"\n",
 							sv->gametype, gametype);
-			if (strcmp (gamename, sv->gamename) != 0)
+			if (gamename[0] != '\0' && strcmp (gamename, sv->gamename) != 0)
 				Com_Printf (MSG_DEBUG,
 							"    Reject: gamename \"%s\" != requested \"%s\"\n",
 							sv->gamename, gamename);
 		}
 
-		// Check protocols, options, and gamename
+		// Check state and protocol
 		if (sv->state <= sv_state_uninitialized ||
-			sv->protocol != protocol ||
-			(! opt_empty && sv->state == sv_state_empty) ||
+			sv->protocol != protocol)
+		{
+			// Skip it
+			continue;
+		}
+
+		// Since the protocols match, if we don't know the game name yet and
+		// that this server doesn't use the DarkPlaces protocol, use its game name
+		// (if the unknown game was using the DP protocol, the client should have
+		// sent a game name with its "getservers" query)
+		if (gamename[0] == '\0' && sv->anon_properties != NULL)
+		{
+			strncpy (gamename, sv->gamename, sizeof (gamename) - 1);
+			gamename[sizeof (gamename) - 1] = '\0';
+
+			Com_Printf (MSG_DEBUG, "  - Using this server's game name\n");
+
+			if (! Game_IsAccepted (gamename))
+			{
+				Com_Printf (MSG_WARNING,
+							"> WARNING: Rejecting %s from %s (game \"%s\" is not accepted)\n",
+							request_name, peer_address, gamename);
+				return;
+			}
+		}
+
+		// Check options, game type and game name
+		if ((! opt_empty && sv->state == sv_state_empty) ||
 			(! opt_full && sv->state == sv_state_full) ||
 			(! opt_ipv4 && sv->address.ss_family == AF_INET) ||
 			(! opt_ipv6 && sv->address.ss_family == AF_INET6) ||
@@ -678,12 +789,36 @@
 	}
 	new_clients = ((value != NULL) ? atoi (value) : 0);
 
-	// Q3A doesn't send a gamename, so we add it manually
+	// If the server didn't send a gamename, guess it using the protocol
 	value = SearchInfostring (msg, "gamename");
 	if (value == NULL)
-		value = GAMENAME_Q3A;
-	else if (value[0] == '\0')
 	{
+		// Games that neither send a known heartbeat nor provide a game name are ignored
+		if (server->hb_properties == NULL)
+		{
+			Com_Printf (MSG_WARNING,
+						"> WARNING: invalid infoResponse from %s (no game name)\n",
+						peer_address);
+			return;
+		}
+		
+		value = server->hb_properties->name;
+	}
+	// ... but if it did, it must match the one its heartbeat advertized (if any)
+	else
+	{
+		if (server->hb_properties != NULL &&
+			strcmp (value, server->hb_properties->name) != 0)
+		{
+			Com_Printf (MSG_WARNING,
+						"> WARNING: invalid infoResponse from %s (game name is different from the one advertized by the heartbeat)\n",
+						peer_address);
+			return;
+		}
+	}
+
+	if (value[0] == '\0')
+	{
 		Com_Printf (MSG_WARNING,
 					"> WARNING: invalid infoResponse from %s (game name is void)\n",
 					peer_address);
@@ -708,6 +843,7 @@
 	// Save some useful informations in the server entry
 	strncpy (server->gamename, value, sizeof (server->gamename) - 1);
 	server->protocol = new_protocol;
+	server->anon_properties = server->hb_properties;
 	strncpy (server->gametype, new_gametype, sizeof (server->gametype) - 1);
 	if (new_clients == 0)
 		server->state = sv_state_empty;
@@ -735,32 +871,18 @@
 					socklen_t addrlen,
 					socket_t recv_socket)
 {
-	server_t* server;
-
 	// If it's an heartbeat
 	if (!strncmp (S2M_HEARTBEAT, msg, strlen (S2M_HEARTBEAT)))
 	{
-		char gameId [64];
-
-		// Extract the game id
-		sscanf (msg + strlen (S2M_HEARTBEAT) + 1, "%63s", gameId);
-		Com_Printf (MSG_NORMAL, "> %s ---> heartbeat (%s)\n",
-					peer_address, gameId);
-
-		// Get the server in the list (add it to the list if necessary)
-		server = Sv_GetByAddr (address, addrlen, true);
-		if (server == NULL)
-			return;
-
-		assert (server->state != sv_state_unused_slot);
-
-		// Ask for some infos
-		SendGetInfo (server, recv_socket);
+		HandleHeartbeat (msg + strlen (S2M_HEARTBEAT), address, addrlen,
+						 recv_socket);
 	}
 
 	// If it's an infoResponse message
 	else if (!strncmp (S2M_INFORESPONSE, msg, strlen (S2M_INFORESPONSE)))
 	{
+		server_t* server;
+
 		Com_Printf (MSG_NORMAL, "> %s ---> infoResponse\n", peer_address);
 	
 		server = Sv_GetByAddr (address, addrlen, false);

Modified: trunk/dpmaster/src/servers.c
===================================================================
--- trunk/dpmaster/src/servers.c	2010-08-22 16:52:33 UTC (rev 10413)
+++ trunk/dpmaster/src/servers.c	2010-08-22 17:06:16 UTC (rev 10414)
@@ -963,7 +963,7 @@
 
 			Com_Printf (msg_level,
 						" (timeout: %lu)\n"
-						"\tgame: \"%s\" (protocol: %d, gametype: %d)\n"
+						"\tgame: \"%s\" (protocol: %d, gametype: %s)\n"
 						"\tstate: %s\n"
 						"\tchallenge: \"%s\" (timeout: %lu)\n",
 						(unsigned long)sv->timeout,

Modified: trunk/dpmaster/src/servers.h
===================================================================
--- trunk/dpmaster/src/servers.h	2010-08-22 16:52:33 UTC (rev 10413)
+++ trunk/dpmaster/src/servers.h	2010-08-22 17:06:16 UTC (rev 10414)
@@ -3,7 +3,7 @@
 
 	Server list and address mapping management for dpmaster
 
-	Copyright (C) 2004-2009  Mathieu Olivier
+	Copyright (C) 2004-2010  Mathieu Olivier
 
 	This program is free software; you can redistribute it and/or modify
 	it under the terms of the GNU General Public License as published by
@@ -71,12 +71,15 @@
 } server_state_t;
 
 // Server properties
+struct game_properties_s;		// Defined in games.h
 typedef struct server_s
 {
 	struct sockaddr_storage address;
 	struct server_s* next;
 	struct server_s** prev_ptr;
 	const struct addrmap_s* addrmap;
+	const struct game_properties_s* anon_properties;	// game properties, for an anonymous game
+	const struct game_properties_s* hb_properties;		// future "anon_properties", not yet validated by an infoResponse
 	time_t timeout;
 	time_t challenge_timeout;
 	socklen_t addrlen;

Modified: trunk/dpmaster/src/system.c
===================================================================
--- trunk/dpmaster/src/system.c	2010-08-22 16:52:33 UTC (rev 10413)
+++ trunk/dpmaster/src/system.c	2010-08-22 17:06:16 UTC (rev 10414)
@@ -3,7 +3,7 @@
 
 	System specific code for dpmaster
 
-	Copyright (C) 2008-2009  Mathieu Olivier
+	Copyright (C) 2008-2010  Mathieu Olivier
 
 	This program is free software; you can redistribute it and/or modify
 	it under the terms of the GNU General Public License as published by
@@ -48,6 +48,9 @@
 // Low privileges user
 static const char* low_priv_user = DEFAULT_LOW_PRIV_USER;
 
+// File descriptor to /dev/null, used by the daemonization process
+static int null_device = -1;
+
 #endif
 
 
@@ -543,6 +546,17 @@
 qboolean Sys_SecurityInit (void)
 {
 #ifndef WIN32
+	// If we will run as a daemon, we need to open /dev/null before chrooting
+	if (daemon_state == DAEMON_STATE_REQUEST)
+	{
+		null_device = open ("/dev/null", O_RDWR, 0);
+		if (null_device == -1)
+		{
+			Com_Printf (MSG_ERROR, "> ERROR: can't open /dev/null\n");
+			return false;
+		}
+	}
+
 	// UNIX allows us to be completely paranoid, so let's go for it
 	if (geteuid () == 0)
 	{
@@ -603,7 +617,7 @@
 	// Should we run as a daemon?
 	if (daemon_state == DAEMON_STATE_REQUEST)
 	{
-		if (daemon (0, 0) != 0)
+		if (daemon (0, 1) != 0)
 		{
 			Com_Printf (MSG_ERROR, "> ERROR: daemonization failed (%s)\n",
 						strerror (errno));
@@ -611,7 +625,17 @@
 			daemon_state = DAEMON_STATE_NO;
 			return false;
 		}
+
+		// Replace the standard input and outputs by /dev/null
+		assert (null_device != -1);
+		dup2 (null_device, STDIN_FILENO);
+		dup2 (null_device, STDOUT_FILENO);
+		dup2 (null_device, STDERR_FILENO);
 		
+		// We no longer need to keep this file descriptor open
+		close (null_device);
+		null_device = -1;
+
 		daemon_state = DAEMON_STATE_EFFECTIVE;
 	}
 #endif

Modified: trunk/dpmaster/testsuite/query_remote_master.pl
===================================================================
--- trunk/dpmaster/testsuite/query_remote_master.pl	2010-08-22 16:52:33 UTC (rev 10413)
+++ trunk/dpmaster/testsuite/query_remote_master.pl	2010-08-22 17:06:16 UTC (rev 10414)
@@ -26,8 +26,10 @@
 
 
 my %defaultProtocols = (
-	"Warsow" => 10,				# can also be 11 (Warsow 0.5)
-	"Quake3Arena" => 68,		# can also be 71 (OpenArena 0.8.1)
+	"Warsow" => 11,
+	"Quake3Arena" => 68,		# can also be 71 (OpenArena 0.8.1+)
+	"RtCW" => 60,
+	"WoET" => 84,
 	
 	# DarkPlaces
 	"DarkPlaces-Quake" => 3,
@@ -40,7 +42,10 @@
 if ($nbArgs < 1 or $nbArgs > 3) {
 	print "Syntax: $0 [options] <game> [protocol number] [master]\n";
 	print "    Ex: $0 Nexuiz\n";
-	print "        $0 Quake3Arena 68\n";
+	print "        $0 Quake3Arena\n";
+	print "        $0 RtCW\n";
+	print "        $0 WoET\n";
+	print "        $0 Warsow 10\n";
 	print "        $0 Warsow 5308 dpmaster.deathmask.net\n";
 	exit;
 }
@@ -69,6 +74,12 @@
 if ($gamename eq "Quake3Arena") {
 	$gamefamily = GAME_FAMILY_QUAKE3ARENA;
 }
+elsif ($gamename eq "RtCW") {
+	$gamefamily = GAME_FAMILY_RTCW;
+}
+elsif ($gamename eq "WoET") {
+	$gamefamily = GAME_FAMILY_WOET;
+}
 else {
 	$gamefamily = GAME_FAMILY_DARKPLACES;
 }

Modified: trunk/dpmaster/testsuite/test-gametype.pl
===================================================================
--- trunk/dpmaster/testsuite/test-gametype.pl	2010-08-22 16:52:33 UTC (rev 10413)
+++ trunk/dpmaster/testsuite/test-gametype.pl	2010-08-22 17:06:16 UTC (rev 10414)
@@ -98,4 +98,4 @@
 	}
 }
 
-Test_Run ("Servers running games from different game families");
+Test_Run ("Servers running games using different gametypes and families");

Modified: trunk/dpmaster/testsuite/test-multiple_game_families.pl
===================================================================
--- trunk/dpmaster/testsuite/test-multiple_game_families.pl	2010-08-22 16:52:33 UTC (rev 10413)
+++ trunk/dpmaster/testsuite/test-multiple_game_families.pl	2010-08-22 17:06:16 UTC (rev 10414)
@@ -45,25 +45,44 @@
 	{
 		family => GAME_FAMILY_QUAKE3ARENA,
 		id => "Q3Server3",
-		protonum => 2,
 	},
+
+	# RTCW servers
+	{
+		family => GAME_FAMILY_RTCW,
+		id => "RtCWServer1",
+		protonum => 19,
+	},
+
+	# WoET servers
+	{
+		family => GAME_FAMILY_WOET,
+		id => "WoETServer1",
+		protonum => 20,
+	},
 );
 
 foreach my $propertiesRef (@serverPropertiesList) {
 	my $serverFamily = $propertiesRef->{family};
+	my $serverId = $propertiesRef->{id};
 	my $serverProtocol = $propertiesRef->{protonum};
 	my $serverGame = $propertiesRef->{game};
 
 	# Create the server
 	my $serverRef = Server_New ($serverFamily);
-	Server_SetProperty ($serverRef, "id", $propertiesRef->{id});
-	Server_SetGameProperty ($serverRef, "protocol", $serverProtocol);
+	Server_SetProperty ($serverRef, "id", $serverId);
+	if (defined $serverProtocol) {
+		Server_SetGameProperty ($serverRef, "protocol", $serverProtocol);
+	}
 	if (defined $serverGame) {
 		Server_SetGameProperty ($serverRef, "gamename", $serverGame);
 	}
 
 	# Create the associated client
+	my $clientId = $serverId;
+	$clientId =~ s/Server/Client/;
 	my $clientRef = Client_New ($serverFamily);
+	Client_SetProperty ($clientRef, "id", $clientId);
 	Client_SetGameProperty ($clientRef, "protocol", $serverProtocol);
 	if (defined $serverGame) {
 		Client_SetGameProperty ($clientRef, "gamename", $serverGame);

Added: trunk/dpmaster/testsuite/test-new_heartbeat.pl
===================================================================
--- trunk/dpmaster/testsuite/test-new_heartbeat.pl	                        (rev 0)
+++ trunk/dpmaster/testsuite/test-new_heartbeat.pl	2010-08-22 17:06:16 UTC (rev 10414)
@@ -0,0 +1,23 @@
+#!/usr/bin/perl -w
+
+use strict;
+use testlib;
+
+
+Master_SetProperty ("extraOptions", [ "-g", "Warsow", "heartbeat=Warsow" ]);
+
+# Server1's heartbeat advertizes the Warsow game, but send another game name. It shouldn't work
+my $server1Ref = Server_New ();
+Server_SetProperty ($server1Ref, "masterProtocol", "Warsow");
+Server_SetGameProperty ($server1Ref, "gamename", "SomethingElse");
+Server_SetProperty ($server1Ref, "cannotBeRegistered", 1);
+
+# Server2's heartbeat advertizes the Warsow game, and uses this game name. It should work
+my $server2Ref = Server_New ();
+Server_SetProperty ($server2Ref, "masterProtocol", "Warsow");
+Server_SetGameProperty ($server2Ref, "gamename", "Warsow");
+
+my $clientRef = Client_New ();
+Client_SetGameProperty ($clientRef, "gamename", "Warsow");
+
+Test_Run ("Server using a new heartbeat");


Property changes on: trunk/dpmaster/testsuite/test-new_heartbeat.pl
___________________________________________________________________
Name: svn:executable
   + *

Added: trunk/dpmaster/testsuite/test-unknown_heartbeat.pl
===================================================================
--- trunk/dpmaster/testsuite/test-unknown_heartbeat.pl	                        (rev 0)
+++ trunk/dpmaster/testsuite/test-unknown_heartbeat.pl	2010-08-22 17:06:16 UTC (rev 10414)
@@ -0,0 +1,12 @@
+#!/usr/bin/perl -w
+
+use strict;
+use testlib;
+
+
+my $serverRef = Server_New ();
+my $clientRef = Client_New ();
+
+Server_SetProperty ($serverRef, "masterProtocol", "Warsow");
+Server_SetProperty ($serverRef, "cannotBeAnswered", 1);
+Test_Run ("Server sending an unknown heartbeat");


Property changes on: trunk/dpmaster/testsuite/test-unknown_heartbeat.pl
___________________________________________________________________
Name: svn:executable
   + *

Modified: trunk/dpmaster/testsuite/testlib.pm
===================================================================
--- trunk/dpmaster/testsuite/testlib.pm	2010-08-22 16:52:33 UTC (rev 10413)
+++ trunk/dpmaster/testsuite/testlib.pm	2010-08-22 17:06:16 UTC (rev 10414)
@@ -23,6 +23,10 @@
 use constant DEFAULT_PROTOCOL => 5;
 use constant QUAKE3ARENA_GAMENAME => "Quake3Arena";
 use constant QUAKE3ARENA_PROTOCOL => 67;
+use constant RTCW_GAMENAME => "wolfmp";
+use constant RTCW_PROTOCOL => 60;
+use constant WOET_GAMENAME => "et";
+use constant WOET_PROTOCOL => 84;
 
 # Constants - misc
 use constant DEFAULT_SERVER_PORT => 5678;
@@ -30,6 +34,8 @@
 use constant {
 	GAME_FAMILY_DARKPLACES => 0,
 	GAME_FAMILY_QUAKE3ARENA => 1,
+	GAME_FAMILY_RTCW => 2,
+	GAME_FAMILY_WOET => 3,
 };
 
 
@@ -98,6 +104,8 @@
 
 		GAME_FAMILY_DARKPLACES
 		GAME_FAMILY_QUAKE3ARENA
+		GAME_FAMILY_RTCW
+		GAME_FAMILY_WOET
 	);
 }
 
@@ -365,11 +373,21 @@
 	$nextClientId++;
 
 	# Game family specific variables
-	my ($gamename, $protocol);
+	my ($gamename, $protocol, $queryFilters);
+	$queryFilters = "empty full";
 	if ($gameFamily == GAME_FAMILY_QUAKE3ARENA) {
 		$gamename = QUAKE3ARENA_GAMENAME;
 		$protocol = QUAKE3ARENA_PROTOCOL;
 	}
+	elsif ($gameFamily == GAME_FAMILY_RTCW) {
+		$gamename = RTCW_GAMENAME;
+		$protocol = RTCW_PROTOCOL;
+	}
+	elsif ($gameFamily == GAME_FAMILY_WOET) {
+		$gamename = WOET_GAMENAME;
+		$protocol = WOET_PROTOCOL;
+		$queryFilters = "";		# WoET never send "empty" and "full"
+	}
 	else {  # $gameFamily == GAME_FAMILY_DARKPLACES
 		$gamename = DEFAULT_GAMENAME;
 		$protocol = DEFAULT_PROTOCOL;
@@ -386,7 +404,7 @@
 		alwaysUseExtendedQuery => 0,
 		cannotBeAnswered => 0,
 		useIPv6 => 0,
-		queryFilters => "empty full",
+		queryFilters => $queryFilters,
 		ignoreEOTMarks => 0,
 
 		gameProperties => {
@@ -482,7 +500,7 @@
 
 	my $gameProp = $clientRef->{gameProperties};
 
-	if ($clientRef->{family} != GAME_FAMILY_QUAKE3ARENA or $useExtendedQuery) {
+	if ($clientRef->{family} == GAME_FAMILY_DARKPLACES or $useExtendedQuery) {
 		if (defined ($gameProp->{gamename})) {
 			$getservers .= " $gameProp->{gamename}";
 		}
@@ -817,6 +835,16 @@
 		$protocol = QUAKE3ARENA_PROTOCOL;
 		$masterProtocol = "QuakeArena-1";
 	}
+	elsif ($gameFamily == GAME_FAMILY_RTCW) {
+		$gamename = RTCW_GAMENAME;
+		$protocol = RTCW_PROTOCOL;
+		$masterProtocol = "Wolfenstein-1";
+	}
+	elsif ($gameFamily == GAME_FAMILY_WOET) {
+		$gamename = WOET_GAMENAME;
+		$protocol = WOET_PROTOCOL;
+		$masterProtocol = "EnemyTerritory-1";
+	}
 	else {  # $gameFamily == GAME_FAMILY_DARKPLACES
 		$gamename = DEFAULT_GAMENAME;
 		$protocol = DEFAULT_PROTOCOL;



More information about the twilight-commits mailing list