From: Mattia Basaglia <mattia.basaglia@gmail.com>
Date: Thu, 5 Feb 2015 14:32:19 +0000 (+0100)
Subject: Minigame code and cfg
X-Git-Tag: xonotic-v0.8.2~2038^2~36
X-Git-Url: https://git.rm.cloudns.org/?a=commitdiff_plain;h=eed7412a8eb06451f75abce0e990b2914b9c963a;p=xonotic%2Fxonotic-data.pk3dir.git

Minigame code and cfg
---

diff --git a/binds-xonotic.cfg b/binds-xonotic.cfg
index f48842f8cd..cc40398e5f 100644
--- a/binds-xonotic.cfg
+++ b/binds-xonotic.cfg
@@ -56,6 +56,7 @@ bind u "+con_chat_maximize"
 bind m +hud_panel_radar_maximized
 bind i +show_info
 bind PAUSE pause
+bind F9 "cl_cmd hud minigame"
 bind F10 menu_showquitdialog
 bind F11 disconnect
 bind F12 screenshot
diff --git a/defaultXonotic.cfg b/defaultXonotic.cfg
index 034b3bb32d..c8321edbc2 100644
--- a/defaultXonotic.cfg
+++ b/defaultXonotic.cfg
@@ -1100,7 +1100,8 @@ seta cl_gentle_damage 0		"client side gentle mode (only replaces damage flash);
 set g_jetpack 0 "Jetpack mutator"
 
 set g_running_guns 0 "... or wonder, till it drives you mad, what would have followed if you had."
-set g_bastet 0 "don't try"
+set sv_minigames 1 "Allow minigames"
+set sv_minigames_observer 1 "Force minigame players to be observers. 0: don't move them to observer, 1: move them to observer, 2: force observer"
 
 set _urllib_nextslot 0 "temp variable"
 set cl_warpzone_usetrace 1 "do not touch"
diff --git a/hud_luma.cfg b/hud_luma.cfg
index 4f801509eb..ec4e415f23 100644
--- a/hud_luma.cfg
+++ b/hud_luma.cfg
@@ -309,4 +309,44 @@ seta hud_panel_buffs_bg_alpha ""
 seta hud_panel_buffs_bg_border ""
 seta hud_panel_buffs_bg_padding ""
 
+seta hud_panel_minigameboard "1"
+seta hud_panel_minigameboard_pos "0.22 0.15"
+seta hud_panel_minigameboard_size "0.50 0.60"
+seta hud_panel_minigameboard_bg "border_small"
+seta hud_panel_minigameboard_bg_color ""
+seta hud_panel_minigameboard_bg_color_team ""
+seta hud_panel_minigameboard_bg_alpha ""
+seta hud_panel_minigameboard_bg_border ""
+seta hud_panel_minigameboard_bg_padding ""
+
+seta hud_panel_minigamestatus "1"
+seta hud_panel_minigamestatus_pos "0.74 0.15"
+seta hud_panel_minigamestatus_size "0.2 0.60"
+seta hud_panel_minigamestatus_bg "border_small"
+seta hud_panel_minigamestatus_bg_color ""
+seta hud_panel_minigamestatus_bg_color_team ""
+seta hud_panel_minigamestatus_bg_alpha ""
+seta hud_panel_minigamestatus_bg_border ""
+seta hud_panel_minigamestatus_bg_padding ""
+
+seta hud_panel_minigamehelp "1"
+seta hud_panel_minigamehelp_pos "0.22 0.78"
+seta hud_panel_minigamehelp_size "0.50 0.20"
+seta hud_panel_minigamehelp_bg ""
+seta hud_panel_minigamehelp_bg_color ""
+seta hud_panel_minigamehelp_bg_color_team ""
+seta hud_panel_minigamehelp_bg_alpha ""
+seta hud_panel_minigamehelp_bg_border ""
+seta hud_panel_minigamehelp_bg_padding ""
+
+seta hud_panel_minigamemenu "0"
+seta hud_panel_minigamemenu_pos "0 0.26"
+seta hud_panel_minigamemenu_size "0.2 0.49"
+seta hud_panel_minigamemenu_bg "border_small"
+seta hud_panel_minigamemenu_bg_color ""
+seta hud_panel_minigamemenu_bg_color_team ""
+seta hud_panel_minigamemenu_bg_alpha ""
+seta hud_panel_minigamemenu_bg_border ""
+seta hud_panel_minigamemenu_bg_padding ""
+
 menu_sync
diff --git a/keybinds.txt b/keybinds.txt
index 189d02eb4d..9cfbccaa9d 100644
--- a/keybinds.txt
+++ b/keybinds.txt
@@ -35,6 +35,7 @@
 "+showscores"                           "show scores"
 "screenshot"                            "screen shot"
 "+hud_panel_radar_maximized"            "maximize radar"
+"cl_cmd hud minigame"                   "toggle minigame menu"
 ""                                      ""
 ""                                      "Communicate"
 "messagemode"                           "public chat"
diff --git a/keybinds.txt.de b/keybinds.txt.de
index 0c2aaf2b18..0f30ce2bf8 100644
--- a/keybinds.txt.de
+++ b/keybinds.txt.de
@@ -35,6 +35,7 @@
 "+showscores"                           "Tabelle anzeigen"
 "screenshot"                            "Bildschirmfoto"
 "+hud_panel_radar_maximized"            "Radar maximieren"
+"cl_cmd hud minigame"                   "Minispiel-Menu an- und ausschalten"
 ""                                      ""
 ""                                      "Kommunikation"
 "messagemode"                           "Nachricht an alle"
diff --git a/keybinds.txt.es b/keybinds.txt.es
index 51d9bfc390..79c3124936 100644
--- a/keybinds.txt.es
+++ b/keybinds.txt.es
@@ -35,6 +35,7 @@
 "+showscores"                           "mostrar puntaje"
 "screenshot"                            "captura de pantalla"
 "+hud_panel_radar_maximized"            "maximize radar (FIXME)"
+"cl_cmd hud minigame"                   "toggle minigame menu (FIXME)"
 ""                                      ""
 ""                                      "Communicación"
 "messagemode"                           "chat público"
diff --git a/keybinds.txt.fr b/keybinds.txt.fr
index 15a21f01e6..b5275b7135 100644
--- a/keybinds.txt.fr
+++ b/keybinds.txt.fr
@@ -35,6 +35,7 @@
 "+showscores"                           "afficher les scores"
 "screenshot"                            "capture d'écran"
 "+hud_panel_radar_maximized"            "agrandir le radar"
+"cl_cmd hud minigame"                   "toggle minigame menu (FIXME)"
 ""                                      ""
 ""                                      "Communication"
 "messagemode"                           "tchat public"
diff --git a/keybinds.txt.hu b/keybinds.txt.hu
index 3ae11f77f7..e22299a447 100644
--- a/keybinds.txt.hu
+++ b/keybinds.txt.hu
@@ -35,6 +35,7 @@
 "+showscores"                           "pontszámok"
 "screenshot"                            "kép mentés"
 "+hud_panel_radar_maximized"            "maximize radar (FIXME)"
+"cl_cmd hud minigame"                   "toggle minigame menu (FIXME)"
 ""                                      ""
 ""                                      "Kommunikáció"
 "messagemode"                           "nyilvános beszélgetés"
diff --git a/keybinds.txt.it b/keybinds.txt.it
index 40f921033c..069f9dfbbb 100644
--- a/keybinds.txt.it
+++ b/keybinds.txt.it
@@ -35,6 +35,7 @@
 "+showscores"                           "mostra punteggi"
 "screenshot"                            "screenshot"
 "+hud_panel_radar_maximized"            "massimizza radar"
+"cl_cmd hud minigame"                   "attiva/disattiva il menù dei giochini"
 ""                                      ""
 ""                                      "Comunicazione"
 "messagemode"                           "chat pubblica"
diff --git a/keybinds.txt.ru b/keybinds.txt.ru
index 7ab93ff8ff..7be181d024 100644
--- a/keybinds.txt.ru
+++ b/keybinds.txt.ru
@@ -35,6 +35,7 @@
 "+showscores"                           "показать очки"
 "screenshot"                            "снимок экрана"
 "+hud_panel_radar_maximized"            "maximize radar (FIXME)"
+"cl_cmd hud minigame"                   "toggle minigame menu (FIXME)"
 ""                                      ""
 ""                                      "Общение"
 "messagemode"                           "общий чат"
diff --git a/qcsrc/client/command/cl_cmd.qc b/qcsrc/client/command/cl_cmd.qc
index 7b74d2dd7f..ecd05c16f5 100644
--- a/qcsrc/client/command/cl_cmd.qc
+++ b/qcsrc/client/command/cl_cmd.qc
@@ -240,6 +240,15 @@ void LocalCommand_hud(int request, int argc)
 					return;
 				}
 
+				case "minigame":
+				{
+					if(HUD_MinigameMenu_IsOpened())
+						HUD_MinigameMenu_Close();
+					else
+						HUD_MinigameMenu_Open();
+					return;
+				}
+
 				case "save":
 				{
 					if(argv(2))
@@ -285,7 +294,7 @@ void LocalCommand_hud(int request, int argc)
 			print("  'configname' is the name to save to for \"save\" action,\n");
 			print("  'radartoggle' is to control hud_panel_radar_maximized for \"radar\" action,\n");
 			print("  and 'layout' is how to organize the scoreboard columns for the set action.\n");
-			print("  Full list of commands here: \"configure, save, scoreboard_columns_help, scoreboard_columns_set, radar.\"\n");
+			print("  Full list of commands here: \"configure, minigame, save, scoreboard_columns_help, scoreboard_columns_set, radar.\"\n");
 			return;
 		}
 	}
diff --git a/qcsrc/client/hud.qc b/qcsrc/client/hud.qc
index 024a0761e4..073789a085 100644
--- a/qcsrc/client/hud.qc
+++ b/qcsrc/client/hud.qc
@@ -4447,12 +4447,30 @@ void HUD_Buffs(void)
 }
 
 
+// Minigame
+//
+#include "../common/minigames/cl_minigames_hud.qc"
+
 /*
 ==================
 Main HUD system
 ==================
 */
 
+float HUD_Panel_CheckFlags(float showflags)
+{
+	if ( HUD_Minigame_Showpanels() )
+		return showflags & PANEL_SHOW_MINIGAME;
+	return showflags & PANEL_SHOW_MAINGAME;
+}
+
+void HUD_Panel_Draw(entity panent)
+{
+	panel = panent;
+	if ( HUD_Panel_CheckFlags(panel.panel_showflags) )
+		panel.panel_draw();
+}
+
 void HUD_Reset (void)
 {
 	// reset gametype specific icons
@@ -4481,12 +4499,17 @@ void HUD_Main (void)
 	// they must fade only when the menu does
 	if(scoreboard_fade_alpha == 1)
 	{
-		(panel = HUD_PANEL(CENTERPRINT)).panel_draw();
+		HUD_Panel_Draw(HUD_PANEL(CENTERPRINT));
 		return;
 	}
 
 	if(!autocvar__hud_configure && !hud_fade_alpha)
+	{
+		hud_fade_alpha = 1;
+		HUD_Panel_Draw(HUD_PANEL(VOTE));
+		hud_fade_alpha = 0;
 		return;
+	}
 
 	// Drawing stuff
 	if (hud_skin_prev != autocvar_hud_skin)
@@ -4586,14 +4609,14 @@ void HUD_Main (void)
 	hud_draw_maximized = 0;
 	// draw panels in order specified by panel_order array
 	for(i = HUD_PANEL_NUM - 1; i >= 0; --i)
-		(panel = hud_panel[panel_order[i]]).panel_draw();
+		HUD_Panel_Draw(hud_panel[panel_order[i]]);
 
 	hud_draw_maximized = 1; // panels that may be maximized must check this var
 	// draw maximized panels on top
 	if(hud_panel_radar_maximized)
-		(panel = HUD_PANEL(RADAR)).panel_draw();
+		HUD_Panel_Draw(HUD_PANEL(RADAR));
 	if(autocvar__con_chat_maximized)
-		(panel = HUD_PANEL(CHAT)).panel_draw();
+		HUD_Panel_Draw(HUD_PANEL(CHAT));
 
 	HUD_Configure_PostDraw();
 
diff --git a/qcsrc/client/hud.qh b/qcsrc/client/hud.qh
index 16a7645fd9..10cc1bc3c6 100644
--- a/qcsrc/client/hud.qh
+++ b/qcsrc/client/hud.qh
@@ -102,29 +102,39 @@ string panel_bg_padding_str;
 float current_player;
 
 float GetPlayerColorForce(int i);
-
+float GetPlayerColor(int i);
+.float panel_showflags;
+const float PANEL_SHOW_NEVER    = 0x00;
+const float PANEL_SHOW_MAINGAME = 0x01;
+const float PANEL_SHOW_MINIGAME = 0x02;
+const float PANEL_SHOW_ALWAYS   = 0xff;
+float HUD_Panel_CheckFlags(float showflags);
 
 #define HUD_PANELS(HUD_PANEL) 																						\
-	HUD_PANEL(WEAPONS      , HUD_Weapons      , weapons) 															\
-	HUD_PANEL(AMMO         , HUD_Ammo         , ammo) 																\
-	HUD_PANEL(POWERUPS     , HUD_Powerups     , powerups) 															\
-	HUD_PANEL(HEALTHARMOR  , HUD_HealthArmor  , healtharmor) 														\
-	HUD_PANEL(NOTIFY       , HUD_Notify       , notify) 															\
-	HUD_PANEL(TIMER        , HUD_Timer        , timer) 																\
-	HUD_PANEL(RADAR        , HUD_Radar        , radar) 																\
-	HUD_PANEL(SCORE        , HUD_Score        , score) 																\
-	HUD_PANEL(RACETIMER    , HUD_RaceTimer    , racetimer) 															\
-	HUD_PANEL(VOTE         , HUD_Vote         , vote) 																\
-	HUD_PANEL(MODICONS     , HUD_ModIcons     , modicons)															\
-	HUD_PANEL(PRESSEDKEYS  , HUD_PressedKeys  , pressedkeys) 														\
-	HUD_PANEL(CHAT         , HUD_Chat         , chat) 																\
-	HUD_PANEL(ENGINEINFO   , HUD_EngineInfo   , engineinfo) 														\
-	HUD_PANEL(INFOMESSAGES , HUD_InfoMessages , infomessages) 														\
-	HUD_PANEL(PHYSICS      , HUD_Physics      , physics) 															\
-	HUD_PANEL(CENTERPRINT  , HUD_CenterPrint  , centerprint) 														\
-	HUD_PANEL(BUFFS        , HUD_Buffs        , buffs)
-
-#define HUD_PANEL(NAME, draw_func, name)																			\
+	HUD_PANEL(WEAPONS      , HUD_Weapons      , weapons,        PANEL_SHOW_MAINGAME ) 								\
+	HUD_PANEL(AMMO         , HUD_Ammo         , ammo,           PANEL_SHOW_MAINGAME ) 								\
+	HUD_PANEL(POWERUPS     , HUD_Powerups     , powerups,       PANEL_SHOW_MAINGAME ) 								\
+	HUD_PANEL(HEALTHARMOR  , HUD_HealthArmor  , healtharmor,    PANEL_SHOW_MAINGAME ) 								\
+	HUD_PANEL(NOTIFY       , HUD_Notify       , notify,         PANEL_SHOW_ALWAYS   ) 								\
+	HUD_PANEL(TIMER        , HUD_Timer        , timer,          PANEL_SHOW_ALWAYS   ) 								\
+	HUD_PANEL(RADAR        , HUD_Radar        , radar,          PANEL_SHOW_MAINGAME ) 								\
+	HUD_PANEL(SCORE        , HUD_Score        , score,          PANEL_SHOW_ALWAYS   ) 								\
+	HUD_PANEL(RACETIMER    , HUD_RaceTimer    , racetimer,      PANEL_SHOW_MAINGAME ) 								\
+	HUD_PANEL(VOTE         , HUD_Vote         , vote,           PANEL_SHOW_ALWAYS   ) 								\
+	HUD_PANEL(MODICONS     , HUD_ModIcons     , modicons,       PANEL_SHOW_MAINGAME ) 								\
+	HUD_PANEL(PRESSEDKEYS  , HUD_PressedKeys  , pressedkeys,    PANEL_SHOW_MAINGAME ) 								\
+	HUD_PANEL(CHAT         , HUD_Chat         , chat,           PANEL_SHOW_ALWAYS   ) 								\
+	HUD_PANEL(ENGINEINFO   , HUD_EngineInfo   , engineinfo,     PANEL_SHOW_ALWAYS   ) 								\
+	HUD_PANEL(INFOMESSAGES , HUD_InfoMessages , infomessages,   PANEL_SHOW_MAINGAME ) 								\
+	HUD_PANEL(PHYSICS      , HUD_Physics      , physics,        PANEL_SHOW_MAINGAME ) 								\
+	HUD_PANEL(CENTERPRINT  , HUD_CenterPrint  , centerprint,    PANEL_SHOW_MAINGAME ) 								\
+	HUD_PANEL(BUFFS        , HUD_Buffs        , buffs,          PANEL_SHOW_MAINGAME ) 								\
+	HUD_PANEL(MINIGAME_BOARD, HUD_MinigameBoard ,minigameboard, PANEL_SHOW_MINIGAME ) 								\
+	HUD_PANEL(MINIGAME_STATUS,HUD_MinigameStatus,minigamestatus,PANEL_SHOW_MINIGAME ) 								\
+	HUD_PANEL(MINIGAME_HELP,  HUD_MinigameHelp  ,minigamehelp,  PANEL_SHOW_MINIGAME ) 								\
+	HUD_PANEL(MINIGAME_MENU,  HUD_MinigameMenu  ,minigamemenu,  PANEL_SHOW_ALWAYS   )
+
+#define HUD_PANEL(NAME, draw_func, name, showflags)																			\
 	int HUD_PANEL_##NAME;																							\
 	void draw_func(void);																							\
 	void RegisterHUD_Panel_##NAME() {																				\
@@ -134,7 +144,8 @@ float GetPlayerColorForce(int i);
 		hud_panelent.classname = "hud_panel"; 																		\
 		hud_panelent.panel_name = #name; 																			\
 		hud_panelent.panel_id = HUD_PANEL_##NAME; 																	\
-		hud_panelent.panel_draw = draw_func;		 																\
+		hud_panelent.panel_draw = draw_func;																		\
+		hud_panelent.panel_showflags = showflags;		 															\
 		HUD_PANEL_NUM++; 																							\
 	} 																												\
 	ACCUMULATE_FUNCTION(RegisterHUD_Panels, RegisterHUD_Panel_##NAME);
diff --git a/qcsrc/client/main.qc b/qcsrc/client/main.qc
index 13f9545faa..78b45c6664 100644
--- a/qcsrc/client/main.qc
+++ b/qcsrc/client/main.qc
@@ -115,6 +115,8 @@ void CSQC_Init(void)
 	CALL_ACCUMULATED_FUNCTION(RegisterHUD_Panels);
 	CALL_ACCUMULATED_FUNCTION(RegisterBuffs);
 
+	initialize_minigames();
+
 	WaypointSprite_Load();
 
 	// precaches
@@ -192,6 +194,9 @@ void Shutdown(void)
 		if (!(calledhooks & HOOK_END))
 			localcmd("\ncl_hook_gameend\n");
 	}
+
+	deactivate_minigame();
+	HUD_MinigameMenu_Close();
 }
 
 .float has_team;
@@ -338,6 +343,9 @@ float CSQC_InputEvent(float bInputType, float nPrimary, float nSecondary)
 	if (MapVote_InputEvent(bInputType, nPrimary, nSecondary))
 		return true;
 
+	if (HUD_Minigame_InputEvent(bInputType, nPrimary, nSecondary))
+		return true;
+
 	if(menu_visible && menu_action)
 		if(menu_action(bInputType, nPrimary, nSecondary))
 			return true;
@@ -842,6 +850,7 @@ void CSQC_Ent_Update(float bIsNewEntity)
 		case ENT_CLIENT_SPAWNEVENT: Ent_ReadSpawnEvent(bIsNewEntity); break;
 		case ENT_CLIENT_NOTIFICATION: Read_Notification(bIsNewEntity); break;
 		case ENT_CLIENT_HEALING_ORB: ent_healer(); break;
+		case ENT_CLIENT_MINIGAME: ent_read_minigame(); break;
 
 		default:
 			//error(strcat(_("unknown entity type in CSQC_Ent_Update: %d\n"), self.enttype));
diff --git a/qcsrc/client/progs.src b/qcsrc/client/progs.src
index f80da18d67..fb9ff9ad88 100644
--- a/qcsrc/client/progs.src
+++ b/qcsrc/client/progs.src
@@ -55,6 +55,9 @@ weapons/projectile.qc // TODO
 ../common/command/markup.qc
 ../common/command/rpn.qc
 
+../common/minigames/minigames.qc
+../common/minigames/cl_minigames.qc
+
 ../common/monsters/monsters.qc
 
 ../common/weapons/weapons.qc // TODO
diff --git a/qcsrc/client/scoreboard.qc b/qcsrc/client/scoreboard.qc
index c6d871807f..fa8d660ed2 100644
--- a/qcsrc/client/scoreboard.qc
+++ b/qcsrc/client/scoreboard.qc
@@ -1,4 +1,5 @@
 #include "scoreboard.qh"
+#include "../common/minigames/cl_minigames.qh"
 
 float scoreboard_alpha_bg;
 float scoreboard_alpha_fg;
@@ -959,7 +960,7 @@ float HUD_WouldDrawScoreboard() {
 		return 1;
 	else if (intermission == 2)
 		return 0;
-	else if (spectatee_status != -1 && getstati(STAT_HEALTH) <= 0 && autocvar_cl_deathscoreboard && gametype != MAPINFO_TYPE_CTS)
+	else if (spectatee_status != -1 && getstati(STAT_HEALTH) <= 0 && autocvar_cl_deathscoreboard && gametype != MAPINFO_TYPE_CTS && !active_minigame)
 		return 1;
 	else if (scoreboard_showscores_force)
 		return 1;
diff --git a/qcsrc/client/view.qc b/qcsrc/client/view.qc
index ee8ef320ae..03b2407707 100644
--- a/qcsrc/client/view.qc
+++ b/qcsrc/client/view.qc
@@ -544,7 +544,9 @@ void UpdateCrosshair()
 			CSQC_common_hud();
 
 	// crosshair goes VERY LAST
-	if(!scoreboard_active && !camera_active && intermission != 2 && spectatee_status != -1 && hud == HUD_NORMAL)
+	if(!scoreboard_active && !camera_active && intermission != 2 && 
+		spectatee_status != -1 && hud == HUD_NORMAL && 
+		!HUD_MinigameMenu_IsOpened() )
 	{
 		if (!autocvar_crosshair_enabled) // main toggle for crosshair rendering
 			return;
@@ -1774,6 +1776,8 @@ void CSQC_UpdateView(float w, float h)
 
 	if(autocvar__hud_configure)
 		HUD_Panel_Mouse();
+	if ( HUD_MinigameMenu_IsOpened() || minigame_isactive() )
+		HUD_Minigame_Mouse();
 
     if(hud && !intermission)
     {
diff --git a/qcsrc/common/constants.qh b/qcsrc/common/constants.qh
index 45a65abbe9..b4e5a72adb 100644
--- a/qcsrc/common/constants.qh
+++ b/qcsrc/common/constants.qh
@@ -78,6 +78,7 @@ const int ENT_CLIENT_ELIMINATEDPLAYERS = 39;
 const int ENT_CLIENT_TURRET = 40;
 const int ENT_CLIENT_AUXILIARYXHAIR = 50;
 const int ENT_CLIENT_VEHICLE = 60;
+const int ENT_CLIENT_MINIGAME = 75;
 
 const int ENT_CLIENT_HEALING_ORB = 80;
 
diff --git a/qcsrc/common/minigames/cl_minigames.qc b/qcsrc/common/minigames/cl_minigames.qc
new file mode 100644
index 0000000000..f6af765b74
--- /dev/null
+++ b/qcsrc/common/minigames/cl_minigames.qc
@@ -0,0 +1,401 @@
+#include "cl_minigames.qh"
+
+// Draw a square in the center of the avaliable area
+void minigame_hud_simpleboard(vector pos, vector mySize, string board_texture)
+{
+	if(panel.current_panel_bg != "0" && panel.current_panel_bg != "")
+		draw_BorderPicture(pos - '1 1 0' * panel_bg_border, 
+					panel.current_panel_bg, 
+					mySize + '1 1 0' * 2 * panel_bg_border, 
+					panel_bg_color, panel_bg_alpha, 
+					 '1 1 0' * (panel_bg_border/BORDER_MULTIPLIER));
+	drawpic(pos, board_texture, mySize, '1 1 1', panel_bg_alpha, DRAWFLAG_NORMAL);
+}
+
+// De-normalize (2D vector) v from relative coordinate inside pos mySize
+vector minigame_hud_denormalize(vector v, vector pos, vector mySize)
+{
+	v_x = pos_x + v_x * mySize_x;
+	v_y = pos_y + v_y * mySize_y;
+	return v;
+}
+// De-normalize (2D vector) v from relative size inside pos mySize
+vector minigame_hud_denormalize_size(vector v, vector pos, vector mySize)
+{
+	v_x = v_x * mySize_x;
+	v_y = v_y * mySize_y;
+	return v;
+}
+
+// Normalize (2D vector) v to relative coordinate inside pos mySize
+vector minigame_hud_normalize(vector v, vector pos, vector mySize)
+{
+	v_x = ( v_x - pos_x ) / mySize_x;
+	v_y = ( v_y - pos_y ) / mySize_y;
+	return v;
+}
+
+// Check if the mouse is inside the given area
+float minigame_hud_mouse_in(vector pos, vector sz)
+{
+	return mousepos_x >= pos_x && mousepos_x < pos_x + sz_x &&
+	       mousepos_y >= pos_y && mousepos_y < pos_y + sz_y ;
+}
+
+void initialize_minigames()
+{
+	entity last_minig = world;
+	entity minig;
+	#define MINIGAME(name,nicename) \
+		minig = spawn(); \
+		minig.classname = "minigame_descriptor"; \
+		minig.netname = strzone(strtolower(#name)); \
+		minig.message = nicename; \
+		minig.minigame_hud_board = minigame_hud_board_##name; \
+		minig.minigame_hud_status = minigame_hud_status_##name; \
+		minig.minigame_event = minigame_event_##name; \
+		if ( !last_minig ) minigame_descriptors = minig; \
+		else last_minig.list_next = minig; \
+		last_minig = minig;
+		
+	REGISTERED_MINIGAMES
+	
+	#undef MINIGAME
+}
+
+string minigame_texture_skin(string skinname, string name)
+{
+	return sprintf("gfx/hud/%s/minigames/%s", skinname, name);
+}
+string minigame_texture(string name)
+{
+	string path = minigame_texture_skin(autocvar_menu_skin,name);
+	if ( precache_pic(path) == "" )
+		path = minigame_texture_skin("default", name);
+	return path;
+}
+
+#define FIELD(Flags, Type, Name) MSLE_CLEAN_##Type(self.Name)
+#define MSLE_CLEAN_String(x) strunzone(x);
+#define MSLE_CLEAN_Byte(x)
+#define MSLE_CLEAN_Char(x)
+#define MSLE_CLEAN_Short(x)
+#define MSLE_CLEAN_Coord(x)
+#define MSLE_CLEAN_Angle(x)
+#define MSLE_CLEAN_Float(x)
+#define MSLE_CLEAN_Vector(x)
+#define MSLE_CLEAN_Vector2D(x)
+
+#define MSLE(Name,Fields) \
+	void msle_entremove_##Name() { strunzone(self.netname); Fields }
+MINIGAME_SIMPLELINKED_ENTITIES
+#undef MSLE
+#undef FIELD
+
+void minigame_autoclean_entity(entity e)
+{
+	dprint("CL Auto-cleaned: ",ftos(num_for_edict(e)), " (",e.classname,")\n");
+	remove(e);
+}
+
+void HUD_MinigameMenu_CurrentButton();
+float auto_close_minigamemenu;
+void deactivate_minigame()
+{
+	if ( !active_minigame )
+		return;
+	
+	active_minigame.minigame_event(active_minigame,"deactivate");
+	entity e = world;
+	while( (e = findentity(e, owner, self)) )
+		if ( e.minigame_autoclean )
+		{
+			minigame_autoclean_entity(e);
+		}
+
+	minigame_self = world;
+	active_minigame = world;
+	
+	if ( auto_close_minigamemenu )
+	{
+		HUD_MinigameMenu_Close();
+		auto_close_minigamemenu = 0;
+	}
+	else
+		HUD_MinigameMenu_CurrentButton();
+}
+
+void activate_minigame(entity minigame)
+{
+	if ( !minigame )
+	{
+		deactivate_minigame();
+		return;
+	}
+	
+	if ( !minigame.descriptor || minigame.classname != "minigame" )
+	{
+		dprint("Trying to activate unregistered minigame ",minigame.netname," in client\n");
+		return;
+	}
+	
+	if ( minigame == active_minigame )
+		return;
+	
+	if ( active_minigame )
+	{
+		entity olds = minigame_self;
+		deactivate_minigame();
+		minigame_self = olds;
+	}
+	
+	if ( minigame_self.owner != minigame )
+		minigame_self = world;
+	active_minigame = minigame;
+	active_minigame.minigame_event(active_minigame,"activate");
+	
+	if ( HUD_MinigameMenu_IsOpened() )
+		HUD_MinigameMenu_CurrentButton();
+	else
+	{
+		auto_close_minigamemenu = 1;
+		HUD_MinigameMenu_Open();
+	}
+}
+
+void minigame_player_entremove()
+{
+	if ( self.owner == active_minigame && self.minigame_playerslot == player_localentnum )
+		deactivate_minigame();
+}
+
+vector ReadVector2D() { vector v; v_x = ReadCoord(); v_y = ReadCoord(); v_z = 0; return v; }
+vector ReadVector() { vector v; v_x = ReadCoord(); v_y = ReadCoord(); v_z = ReadCoord(); return v; }
+string() ReadString_Raw = #366;
+string ReadString_Zoned() { return strzone(ReadString_Raw()); }
+#define ReadFloat ReadCoord
+#define ReadString ReadString_Zoned
+#define FIELD(Flags, Type,Name) if ( sf & (Flags) ) self.Name = Read##Type();
+#define MSLE(Name,Fields) \
+	else if ( self.classname == #Name ) { \
+		if ( sf & MINIG_SF_CREATE ) { \
+			minigame_read_owner(); \
+			self.entremove = msle_entremove_##Name; \
+		} \
+		minigame_ent = self.owner; \
+		Fields \
+	}
+void minigame_read_owner()
+{
+	string owner_name = ReadString_Raw();
+	self.owner = world;
+	do
+		self.owner = find(self.owner,netname,owner_name);
+	while ( self.owner && self.owner.classname != "minigame" );
+	if ( !self.owner )
+		dprint("Got a minigame entity without a minigame!\n");
+}
+void ent_read_minigame()
+{
+	float sf = ReadByte();
+	if ( sf & MINIG_SF_CREATE )
+	{
+		self.classname = msle_classname(ReadShort());
+		self.netname = ReadString_Zoned();
+	}
+	
+	entity minigame_ent = world;
+	
+	if ( self.classname == "minigame" )
+	{
+		minigame_ent = self;
+		
+		if ( sf & MINIG_SF_CREATE )
+		{
+			self.entremove = deactivate_minigame;
+			self.descriptor = minigame_get_descriptor(ReadString_Raw());
+			if ( !self.descriptor )
+				dprint("Got a minigame without a client-side descriptor!\n");
+			else
+				self.minigame_event = self.descriptor.minigame_event;
+		}
+		if ( sf & MINIG_SF_UPDATE )
+			self.minigame_flags = ReadLong();
+	}
+	else if ( self.classname == "minigame_player" )
+	{
+		float activate = 0;
+		if ( sf & MINIG_SF_CREATE )
+		{
+			self.entremove = minigame_player_entremove;
+			minigame_read_owner();
+			float ent = ReadLong();
+			self.minigame_playerslot = ent;
+			dprint("Player: ",GetPlayerName(ent-1),"\n");
+			
+			activate = (ent == player_localnum+1 && self.owner && self.owner != active_minigame);
+			
+		}
+		minigame_ent = self.owner;
+			
+		if ( sf & MINIG_SF_UPDATE )
+			self.team = ReadByte();
+		
+		if ( activate )
+		{
+			minigame_self = self;
+			activate_minigame(self.owner);
+		}
+	}
+	MINIGAME_SIMPLELINKED_ENTITIES
+	
+	if ( minigame_ent )
+		minigame_ent.minigame_event(minigame_ent,"network_receive",self,sf);
+	
+	dprint("CL Reading entity: ",ftos(num_for_edict(self)),
+		" classname:",self.classname," enttype:",ftos(self.enttype) );
+	dprint(" sf:",ftos(sf)," netname:",self.netname,"\n\n");
+}
+#undef ReadFloat
+#undef ReadString
+#undef FIELD
+#undef MSLE
+
+string minigame_getWrappedLine(float w, vector theFontSize, textLengthUpToWidth_widthFunction_t tw)
+{
+	float last_word;
+	string s;
+	float take_until;
+	float skip = 0;
+
+	s = getWrappedLine_remaining;
+
+	if(w <= 0)
+	{
+		getWrappedLine_remaining = string_null;
+		return s; // the line has no size ANYWAY, nothing would be displayed.
+	}
+
+	take_until = textLengthUpToWidth(s, w, theFontSize, tw);
+	
+	if ( take_until > strlen(s) )
+		take_until = strlen(s);
+	
+	float i;
+	for ( i = 0; i < take_until; i++ )
+		if ( substring(s,i,1) == "\n" )
+		{
+			take_until = i;
+			skip = 1;
+			break;
+		}
+	
+	if ( take_until > 0 || skip > 0 )
+	{
+		if ( skip == 0 && take_until < strlen(s) )
+		{
+			last_word = take_until;
+			while(last_word > 0 && substring(s, last_word, 1) != " ")
+				--last_word;
+
+			if ( last_word != 0 )
+			{
+				take_until = last_word;
+				skip = 1;
+			}
+		}
+			
+		getWrappedLine_remaining = substring(s, take_until+skip, strlen(s) - (take_until+skip));
+		if(getWrappedLine_remaining == "")
+			getWrappedLine_remaining = string_null;
+		else if (tw("^7", theFontSize) == 0)
+			getWrappedLine_remaining = strcat(find_last_color_code(substring(s, 0, take_until)), getWrappedLine_remaining);
+		return substring(s, 0, take_until);
+	}
+	else
+	{
+		getWrappedLine_remaining = string_null;
+		return s;
+	}
+}
+
+vector minigame_drawstring_wrapped( float maxwidth, vector pos, string text, 
+	vector fontsize, vector color, float theAlpha, float drawflags, float align )
+{	
+	getWrappedLine_remaining = text;
+	vector mypos = pos;
+	while ( getWrappedLine_remaining )
+	{
+		string line = minigame_getWrappedLine(maxwidth,fontsize,stringwidth_nocolors);
+		if ( line == "" )
+			break;
+		mypos_x = pos_x + (maxwidth - stringwidth_nocolors(line, fontsize)) * align;
+		drawstring(mypos, line, fontsize, color, theAlpha, drawflags);
+		mypos_y += fontsize_y;
+	}
+	mypos_x = maxwidth;
+	mypos_y -= pos_y;
+	return mypos;
+}
+
+vector minigame_drawcolorcodedstring_wrapped( float maxwidth, vector pos, 
+	string text, vector fontsize, float theAlpha, float drawflags, float align )
+{
+	getWrappedLine_remaining = text;
+	vector mypos = pos;
+	while ( getWrappedLine_remaining )
+	{
+		string line = minigame_getWrappedLine(maxwidth,fontsize,stringwidth_colors);
+		if ( line == "" )
+			break;
+		mypos_x = pos_x + (maxwidth - stringwidth_colors(line, fontsize)) * align;
+		drawcolorcodedstring(mypos, line, fontsize, theAlpha, drawflags);
+		mypos_y += fontsize_y;
+	}
+	mypos_x = maxwidth;
+	mypos_y -= pos_y;
+	return mypos;
+}
+
+void minigame_drawstring_trunc(float maxwidth, vector pos, string text, 
+	vector fontsize, vector color, float theAlpha, float drawflags )
+{
+	string line = textShortenToWidth(text,maxwidth,fontsize,stringwidth_nocolors);
+	drawstring(pos, line, fontsize, color, theAlpha, drawflags);
+}
+
+void minigame_drawcolorcodedstring_trunc(float maxwidth, vector pos, string text, 
+	vector fontsize, float theAlpha, float drawflags )
+{
+	string line = textShortenToWidth(text,maxwidth,fontsize,stringwidth_colors);
+	drawcolorcodedstring(pos, line, fontsize, theAlpha, drawflags);
+}
+
+void minigame_drawpic_centered( vector pos, string texture, vector sz, 
+	vector color, float thealpha, float drawflags )
+{
+	drawpic( pos-sz/2, texture, sz, color, thealpha, drawflags );
+}
+
+// Workaround because otherwise variadic arguments won't work properly
+// It could be a bug in the compiler or in darkplaces
+void minigame_cmd_workaround(float dummy, string...cmdargc)
+{
+	string cmd;
+	cmd = "cmd minigame ";
+	float i;
+	for ( i = 0; i < cmdargc; i++ )
+		cmd = strcat(cmd,...(i,string));
+	localcmd(strcat(cmd,"\n"));
+}
+
+// Prompt the player to play in the current minigame 
+// (ie: it's their turn and they should get back to the minigame)
+void minigame_prompt()
+{
+	if ( active_minigame && ! HUD_MinigameMenu_IsOpened() )
+	{
+		HUD_Notify_Push(sprintf("minigames/%s/icon_notif",active_minigame.descriptor.netname),
+			_("It's your turn"), "");
+	}
+}
\ No newline at end of file
diff --git a/qcsrc/common/minigames/cl_minigames.qh b/qcsrc/common/minigames/cl_minigames.qh
new file mode 100644
index 0000000000..404f24a619
--- /dev/null
+++ b/qcsrc/common/minigames/cl_minigames.qh
@@ -0,0 +1,119 @@
+#ifndef CL_MINIGAMES_H
+#define CL_MINIGAMES_H
+
+// Get a square in the center of the avaliable area
+// \note macro to pass by reference pos and mySize
+#define minigame_hud_fitsqare(pos, mySize) \
+	if ( mySize##_x > mySize##_y ) \
+	{ \
+		pos##_x += (mySize##_x-mySize##_y)/2; \
+		mySize##_x = mySize##_y; \
+	} \
+	else \
+	{ \
+		pos##_y += (mySize##_y-mySize##_x)/2; \
+		mySize##_x = mySize##_x; \
+	} \
+	if(panel_bg_padding) \
+	{ \
+		pos += '1 1 0' * panel_bg_padding; \
+		mySize -= '2 2 0' * panel_bg_padding; \
+	}
+
+// Get position and size of a panel
+// \note macro to pass by reference pos and mySize
+#define minigame_hud_panelarea(pos, mySize, panelID) \
+	pos = stov(cvar_string(strcat("hud_panel_", HUD_PANEL(panelID).panel_name, "_pos"))); \
+	mySize = stov(cvar_string(strcat("hud_panel_", HUD_PANEL(panelID).panel_name, "_size"))); \
+	pos##_x *= vid_conwidth; pos##_y *= vid_conheight; \
+	mySize##_x *= vid_conwidth; mySize##_y *= vid_conheight;
+
+// draw a panel border and the given texture
+void minigame_hud_simpleboard(vector pos, vector mySize, string board_texture);
+
+// Normalize (2D vector) v to relative coordinate inside pos mySize
+vector minigame_hud_normalize(vector v, vector pos, vector mySize);
+
+// De-normalize (2D vector) v from relative coordinate inside pos mySize
+vector minigame_hud_denormalize(vector v, vector pos, vector mySize);
+
+// De-normalize (2D vector) v from relative size inside pos mySize
+vector minigame_hud_denormalize_size(vector v, vector pos, vector mySize);
+
+// Check if the mouse is inside the given area
+float minigame_hud_mouse_in(vector pos, vector sz);
+
+// Like drawstring, but wrapping words to fit maxwidth
+// returns the size of the drawn area
+// align selects the string alignment (0 = left, 0.5 = center, 1 = right)
+vector minigame_drawstring_wrapped( float maxwidth, vector pos, string text, 
+	vector fontsize, vector color, float theAlpha, float drawflags, float align );
+
+// Like drawcolorcodedstring, but wrapping words to fit maxwidth
+// returns the size of the drawn area
+// align selects the string alignment (0 = left, 0.5 = center, 1 = right)
+vector minigame_drawcolorcodedstring_wrapped( float maxwidth, vector pos, 
+	string text, vector fontsize, float theAlpha, float drawflags, float align );
+
+// Like drawstring but truncates the text to fit maxwidth
+void minigame_drawstring_trunc(float maxwidth, vector pos, string text, 
+	vector fontsize, vector color, float theAlpha, float drawflags );
+
+// Like drawcolorcodedstring but truncates the text to fit maxwidth
+void minigame_drawcolorcodedstring_trunc(float maxwidth, vector pos, string text, 
+	vector fontsize, float theAlpha, float drawflags );
+
+// like drawpic but pos represent the center rather than the topleft corner
+void minigame_drawpic_centered( vector pos, string texture, vector sz, 
+	vector color, float thealpha, float drawflags );
+
+// Get full path of a minigame texture
+string minigame_texture(string name);
+
+// For minigame descriptors: hud function for the game board
+.void(vector pos, vector size) minigame_hud_board;
+// For minigame descriptors: hud function for the game status
+.void(vector pos, vector size) minigame_hud_status;
+// For minigame_player: player server slot, don't use for anything else
+.float minigame_playerslot;
+
+// register all minigames
+void initialize_minigames();
+
+// client-side minigame session cleanup
+void deactivate_minigame();
+
+// Currently active minigame session
+entity active_minigame;
+// minigame_player representing this client
+entity minigame_self;
+
+// Whethere there's an active minigame
+float minigame_isactive()
+{
+	return active_minigame != world;
+}
+
+// Execute a minigame command
+#define minigame_cmd(...) minigame_cmd_workaround(0,__VA_ARGS__)
+void minigame_cmd_workaround(float dummy, string...cmdargc);
+
+// Read a minigame entity from the server
+void ent_read_minigame();
+
+// Prompt the player to play in the current minigame 
+// (ie: it's their turn and they should get back to the minigame)
+void minigame_prompt();
+
+float HUD_MinigameMenu_IsOpened();
+void HUD_MinigameMenu_Close();
+float HUD_Minigame_Showpanels();
+// Adds a game-specific entry to the menu
+void HUD_MinigameMenu_CustomEntry(entity parent, string message, string event_arg);
+
+
+#define FOREACH_MINIGAME_ENTITY(entityvar) \
+	entityvar=world; \
+	while( (entityvar = findentity(entityvar,owner,active_minigame)) ) 
+
+#endif
diff --git a/qcsrc/common/minigames/cl_minigames_hud.qc b/qcsrc/common/minigames/cl_minigames_hud.qc
new file mode 100644
index 0000000000..7df7609794
--- /dev/null
+++ b/qcsrc/common/minigames/cl_minigames_hud.qc
@@ -0,0 +1,699 @@
+#include "minigames.qh"
+#include "../../client/mapvoting.qh"
+
+float HUD_mouse_over(entity somepanel)
+{
+	vector pos = stov(cvar_string(strcat("hud_panel_", somepanel.panel_name, "_pos")));
+	vector sz = stov(cvar_string(strcat("hud_panel_", somepanel.panel_name, "_size")));
+	return mousepos_x >= pos_x*vid_conwidth  && mousepos_x <= (pos_x+sz_x)*vid_conwidth && 
+	       mousepos_y >= pos_y*vid_conheight && mousepos_y <= (pos_y+sz_y)*vid_conheight ;
+}
+
+// ====================================================================
+// Minigame Board
+// ====================================================================
+
+void HUD_MinigameBoard ()
+{
+	entity hud_minigame = world;
+	
+	if(!autocvar__hud_configure)
+		hud_minigame = active_minigame.descriptor;
+	else
+		hud_minigame = minigame_get_descriptor("nmm");
+	
+	if ( !hud_minigame )
+		return;
+	
+	HUD_Panel_UpdateCvars();
+	
+	
+	vector pos, mySize;
+	pos = panel_pos;
+	mySize = panel_size;
+	
+	hud_minigame.minigame_hud_board(pos,mySize);
+}
+
+// ====================================================================
+// Minigame Status
+// ====================================================================
+void HUD_MinigameStatus ()
+{
+	entity hud_minigame = world;
+	
+	if(!autocvar__hud_configure)
+		hud_minigame = active_minigame.descriptor;
+	else
+		hud_minigame = minigame_get_descriptor("nmm");
+	
+	if ( !hud_minigame )
+		return;
+	
+	HUD_Panel_UpdateCvars();
+	
+	
+	vector pos, mySize;
+	pos = panel_pos;
+	mySize = panel_size;
+	
+	if(panel_bg_padding)
+	{
+		pos += '1 1 0' * panel_bg_padding;
+		mySize -= '2 2 0' * panel_bg_padding;
+	}
+	
+	hud_minigame.minigame_hud_status(pos,mySize);
+}
+
+// ====================================================================
+// Minigame Menu
+// ====================================================================
+
+// Minigame menu options: list head
+entity HUD_MinigameMenu_entries;
+// Minigame menu options: list tail
+entity HUD_MinigameMenu_last_entry;
+
+// Minigame menu options: insert entry after the given location
+void HUD_MinigameMenu_InsertEntry(entity new, entity prev)
+{
+	if ( !HUD_MinigameMenu_entries )
+	{
+		HUD_MinigameMenu_entries = new;
+		HUD_MinigameMenu_last_entry = new;
+		return;
+	}
+	
+	new.list_prev = prev;
+	new.list_next = prev.list_next;
+	if ( prev.list_next )
+		prev.list_next.list_prev = new;
+	else
+		HUD_MinigameMenu_last_entry = new;
+	prev.list_next = new;
+	
+}
+
+
+// minigame menu item uder the mouse
+entity HUD_MinigameMenu_activeitem;
+
+// Click the given item
+void HUD_MinigameMenu_Click(entity menuitem)
+{
+	if ( menuitem )
+	{
+		entity e = self;
+		self = menuitem;
+		menuitem.use();
+		self = e;
+	}
+}
+
+// Minigame menu options: Remove the given entry
+// Precondition: the given entry is actually in the list
+void HUD_MinigameMenu_EraseEntry ( entity e )
+{
+	// remove child items (if any)
+	if ( e.flags & 2 )
+	{
+		HUD_MinigameMenu_Click(e);
+	}
+	
+	if ( e.list_prev )
+		e.list_prev.list_next = e.list_next;
+	else
+		HUD_MinigameMenu_entries = e.list_next;
+				
+	if ( e.list_next )
+		e.list_next.list_prev = e.list_prev;
+	else
+		HUD_MinigameMenu_last_entry = e.list_prev;
+	
+	if ( HUD_MinigameMenu_activeitem == e )
+		HUD_MinigameMenu_activeitem = world;
+	
+	remove(e);
+}
+
+// Minigame menu options: create entry
+entity HUD_MinigameMenu_SpawnEntry(string s, vector offset, vector fontsize, vector color,void() click)
+{
+	entity entry = spawn();
+	entry.message = s;
+	entry.origin = offset;
+	entry.size = fontsize;
+	entry.colormod = color;
+	entry.flags = 0;
+	entry.use = click;
+	panel_pos_y += fontsize_y;
+	return entry;
+}
+
+// Spawn a child entry of a collapsable entry
+entity HUD_MinigameMenu_SpawnSubEntry(string s, void() click, entity parent)
+{
+	vector item_fontsize = hud_fontsize*1.25;
+	vector item_offset = '1 0 0' * item_fontsize_x;
+	entity item = HUD_MinigameMenu_SpawnEntry(
+				s,item_offset,item_fontsize,'0.8 0.8 0.8', click );
+	item.owner = parent;
+	return item;
+}
+
+// Click action for Create sub-entries
+void HUD_MinigameMenu_ClickCreate_Entry()
+{
+	minigame_cmd("create ",self.netname);
+}
+
+// Helper click action for collapsible entries
+// returns true when you have to create the sub-entries
+float HUD_MinigameMenu_Click_ExpandCollapse()
+{
+	entity e;
+	if ( self.flags & 2 )
+	{
+		if ( HUD_MinigameMenu_activeitem && 
+				HUD_MinigameMenu_activeitem.owner == self )
+			HUD_MinigameMenu_activeitem = world;
+		self.flags &= ~2;
+		for ( e = self.list_next; e != world && e.owner == self; e = self.list_next )
+		{
+			if ( e.flags & 2 )
+				HUD_MinigameMenu_Click(e);
+			self.list_next = e.list_next;
+			remove(e);
+		}
+		if ( self.list_next )
+			self.list_next.list_prev = self;
+	}
+	else
+	{
+		for ( e = HUD_MinigameMenu_entries; e != world; e = e.list_next )
+		{
+			if ( e.flags & 2 && e.origin_x == self.origin_x)
+				HUD_MinigameMenu_Click(e);
+		}
+		
+		self.flags |= 2;
+		
+		return true;
+	}
+	return false;
+}
+
+// Click action for the Create menu
+void HUD_MinigameMenu_ClickCreate()
+{
+	if ( HUD_MinigameMenu_Click_ExpandCollapse() )
+	{
+		entity e;
+		entity curr;
+		entity prev = self;
+		for ( e = minigame_descriptors; e != world; e = e.list_next )
+		{
+			curr = HUD_MinigameMenu_SpawnSubEntry(
+				e.message, HUD_MinigameMenu_ClickCreate_Entry,  self );
+			curr.netname = e.netname;
+			curr.model = strzone(minigame_texture(strcat(e.netname,"/icon")));
+			HUD_MinigameMenu_InsertEntry( curr, prev );
+			prev = curr;
+		}
+	}
+}
+
+// Click action for Join sub-entries
+void HUD_MinigameMenu_ClickJoin_Entry()
+{
+	minigame_cmd("join ",self.netname);
+	HUD_MinigameMenu_EraseEntry(self);
+}
+
+// Click action for the Join menu
+void HUD_MinigameMenu_ClickJoin()
+{
+	if ( HUD_MinigameMenu_Click_ExpandCollapse() )
+	{
+		entity e = world;
+		entity curr;
+		entity prev = self;
+		while( (e = find(e,classname,"minigame")) )
+		{
+			if ( e != active_minigame )
+			{
+				curr = HUD_MinigameMenu_SpawnSubEntry(
+					e.netname, HUD_MinigameMenu_ClickJoin_Entry, self );
+				curr.netname = e.netname;
+				curr.model = strzone(minigame_texture(strcat(e.descriptor.netname,"/icon")));
+				HUD_MinigameMenu_InsertEntry( curr, prev );
+				prev = curr;
+			}
+		}
+	}
+}
+
+/*// Temporary placeholder for un-implemented Click actions
+void HUD_MinigameMenu_ClickNoop()
+{
+	dprint("Placeholder for ",self.message,"\n");
+}*/
+
+// Click action for Quit
+void HUD_MinigameMenu_ClickQuit()
+{
+	minigame_cmd("end");
+}
+
+// Click action for Invite sub-entries
+void HUD_MinigameMenu_ClickInvite_Entry()
+{
+	minigame_cmd("invite #",self.netname);
+}
+
+// Click action for the Invite menu
+void HUD_MinigameMenu_ClickInvite()
+{
+	if ( HUD_MinigameMenu_Click_ExpandCollapse() )
+	{
+		float i;
+		entity e;
+		entity prev = self;
+		for(i = 0; i < maxclients; ++i)
+		{
+			if ( player_localnum != i && playerslots[i] && GetPlayerName(i) != "" &&
+				!findfloat(world,minigame_playerslot,i+1) && playerslots[i].ping )
+			{
+				e = HUD_MinigameMenu_SpawnSubEntry(
+					strzone(GetPlayerName(i)), HUD_MinigameMenu_ClickInvite_Entry,
+					self );
+				e.flags |= 1;
+				e.netname = strzone(ftos(i+1));
+				e.origin_x *= 2;
+				HUD_MinigameMenu_InsertEntry(e,prev);
+				prev = e;
+			}
+		}
+	}
+}
+
+void HUD_MinigameMenu_ClickCustomEntry()
+{
+	if ( active_minigame )
+		active_minigame.minigame_event(active_minigame,"menu_click",self.netname);
+}
+
+// Adds a game-specific entry to the menu
+void HUD_MinigameMenu_CustomEntry(entity parent, string menumessage, string event_arg)
+{
+	entity e = HUD_MinigameMenu_SpawnSubEntry(
+		menumessage, HUD_MinigameMenu_ClickCustomEntry, parent );
+	e.netname = event_arg;
+	HUD_MinigameMenu_InsertEntry(e, parent);
+	dprint("CustomEntry ",ftos(num_for_edict(parent))," ",menumessage," ",event_arg,"\n");
+}
+
+// Click action for the Current Game menu
+void HUD_MinigameMenu_ClickCurrentGame()
+{
+	if ( HUD_MinigameMenu_Click_ExpandCollapse() )
+	{
+		HUD_MinigameMenu_InsertEntry( HUD_MinigameMenu_SpawnSubEntry(
+			_("Quit"), HUD_MinigameMenu_ClickQuit, self ), self);
+		
+		active_minigame.minigame_event(active_minigame,"menu_show",self);
+		
+		HUD_MinigameMenu_InsertEntry( HUD_MinigameMenu_SpawnSubEntry(
+			_("Invite"), HUD_MinigameMenu_ClickInvite, self), self);
+	}
+}
+// Whether the minigame menu panel is open
+float HUD_MinigameMenu_IsOpened()
+{
+	return !!HUD_MinigameMenu_entries;
+}
+
+// Close the minigame menu panel
+void HUD_MinigameMenu_Close()
+{
+	if ( HUD_MinigameMenu_IsOpened() )
+	{
+		entity e, p;
+		for ( e = HUD_MinigameMenu_entries; e != world; e = p )
+		{
+			p = e.list_next;
+			remove(e);
+		}
+		HUD_MinigameMenu_entries = world;
+		HUD_MinigameMenu_last_entry = world;
+		HUD_MinigameMenu_activeitem = world;
+		if(autocvar_hud_cursormode)
+		if ( !autocvar__hud_configure )
+			setcursormode(0);
+	}
+}
+
+// toggle a button to manage the current game
+void HUD_MinigameMenu_CurrentButton()
+{
+	entity e;
+	if ( active_minigame )
+	{
+		for ( e = HUD_MinigameMenu_last_entry; e != world; e = e.list_prev )
+			if ( e.classname == "hud_minigamemenu_exit" )
+			{
+				HUD_MinigameMenu_EraseEntry(e);
+				break;
+			}
+		entity currb = HUD_MinigameMenu_SpawnEntry(
+			_("Current Game"), '0 0 0', hud_fontsize*1.5,'0.7 0.84 1', HUD_MinigameMenu_ClickCurrentGame );
+		currb.classname = "hud_minigamemenu_current";
+		currb.model = strzone(minigame_texture(strcat(active_minigame.descriptor.netname,"/icon")));
+		HUD_MinigameMenu_InsertEntry(currb,HUD_MinigameMenu_last_entry);
+		HUD_MinigameMenu_Click(currb);
+	}
+	else 
+	{
+		entity p;
+		for ( e = HUD_MinigameMenu_last_entry; e != world; e = p.list_prev )
+		{
+			p = e;
+			if ( e.classname == "hud_minigamemenu_current" )
+			{
+				p = e.list_next;
+				if ( !p )
+					p = HUD_MinigameMenu_last_entry;
+				HUD_MinigameMenu_EraseEntry(e);
+				break;
+			}
+		}
+		for ( e = HUD_MinigameMenu_last_entry; e != world; e = e.list_prev )
+			if ( e.classname == "hud_minigamemenu_exit" )
+				return;
+		entity exit = HUD_MinigameMenu_SpawnEntry(
+			_("Exit Menu"),'0 0 0',hud_fontsize*1.5,'0.7 0.84 1', HUD_MinigameMenu_Close);
+		exit.classname = "hud_minigamemenu_exit";
+		HUD_MinigameMenu_InsertEntry ( exit, HUD_MinigameMenu_last_entry );
+	}
+}
+
+// Open the minigame menu panel
+void HUD_MinigameMenu_Open()
+{
+	if ( !HUD_MinigameMenu_IsOpened() )
+	{
+		HUD_MinigameMenu_InsertEntry( HUD_MinigameMenu_SpawnEntry(
+			_("Create"), '0 0 0', hud_fontsize*1.5,'0.7 0.84 1', HUD_MinigameMenu_ClickCreate),
+			HUD_MinigameMenu_last_entry );
+		HUD_MinigameMenu_InsertEntry ( HUD_MinigameMenu_SpawnEntry(
+			_("Join"),'0 0 0',hud_fontsize*1.5,'0.7 0.84 1', HUD_MinigameMenu_ClickJoin),
+			HUD_MinigameMenu_last_entry );
+		HUD_MinigameMenu_CurrentButton();
+		HUD_MinigameMenu_activeitem = world;
+		if(autocvar_hud_cursormode)
+			setcursormode(1);
+	}
+}
+
+// Handles mouse input on to minigame menu panel
+void HUD_MinigameMenu_MouseInput()
+{
+	panel = HUD_PANEL(MINIGAME_MENU);
+
+	HUD_Panel_UpdateCvars();
+
+	if(panel_bg_padding)
+	{
+		panel_pos += '1 1 0' * panel_bg_padding;
+		panel_size -= '2 2 0' * panel_bg_padding;
+	}
+	
+	entity e;
+	
+	panel_pos_y += hud_fontsize_y*2;
+	
+	HUD_MinigameMenu_activeitem = world;
+	vector sz;
+	for ( e = HUD_MinigameMenu_entries; e != world; e = e.list_next )
+	{
+		sz = eX*panel_size_x + eY*e.size_y;
+		if ( e.model )
+			sz_y = 22;
+		if ( !HUD_MinigameMenu_activeitem && mousepos_y >= panel_pos_y && mousepos_y <= panel_pos_y + sz_y )
+		{
+			HUD_MinigameMenu_activeitem = e;
+		}
+		panel_pos_y += sz_y;
+	}
+}
+
+// Draw a menu entry
+void HUD_MinigameMenu_DrawEntry(vector pos, string s, vector fontsize, vector color)
+{
+	minigame_drawstring_trunc(panel_size_x-pos_x+panel_pos_x, pos, s,
+							  fontsize, color, panel_fg_alpha, DRAWFLAG_NORMAL);
+}
+// Draw a color-coded menu
+void HUD_MinigameMenu_DrawColoredEntry(vector pos, string s, vector fontsize)
+{
+	minigame_drawcolorcodedstring_trunc(panel_size_x-pos_x+panel_pos_x, pos, s,
+							  fontsize, panel_fg_alpha, DRAWFLAG_NORMAL);
+}
+
+// minigame menu panel UI
+void HUD_MinigameMenu ()
+{	
+	if ( !HUD_MinigameMenu_IsOpened() )
+		return;
+	
+	HUD_Panel_UpdateCvars();
+	
+	HUD_Panel_DrawBg(1);
+	
+	if(panel_bg_padding)
+	{
+		panel_pos += '1 1 0' * panel_bg_padding;
+		panel_size -= '2 2 0' * panel_bg_padding;
+	}
+
+	HUD_MinigameMenu_DrawEntry(panel_pos,_("Minigames"),hud_fontsize*2,'0.25 0.47 0.72');
+	panel_pos_y += hud_fontsize_y*2;
+	
+	entity e;
+	vector color;
+	vector offset;
+	float itemh;
+	vector imgsz = '22 22 0'; // NOTE: if changed, edit where HUD_MinigameMenu_activeitem is selected
+	for ( e = HUD_MinigameMenu_entries; e != world; e = e.list_next )
+	{
+		color = e.colormod;
+		
+		offset = e.origin;
+		itemh = e.size_y;
+		
+		if ( e.model )
+			itemh = imgsz_y;
+		
+		if ( e.flags & 2 )
+		{
+			drawfill(panel_pos, eX*panel_size_x + eY*itemh, e.colormod, 
+					panel_fg_alpha, DRAWFLAG_NORMAL);
+			color = '0 0 0';
+		}
+
+		if ( e.model )
+		{
+			drawpic( panel_pos+offset, e.model, imgsz, '1 1 1', panel_fg_alpha, DRAWFLAG_NORMAL );
+			offset_x += imgsz_x;
+			offset_y = (imgsz_y-e.size_y) / 2;
+		}
+		
+		if ( e.flags & 1 )
+			HUD_MinigameMenu_DrawColoredEntry(panel_pos+offset,e.message,e.size);
+		else
+			HUD_MinigameMenu_DrawEntry(panel_pos+offset,e.message,e.size,color);
+		
+		if ( e == HUD_MinigameMenu_activeitem )
+			drawfill(panel_pos, eX*panel_size_x + eY*itemh,'1 1 1', 0.25, DRAWFLAG_ADDITIVE);
+		
+		panel_pos_y += itemh;
+	}
+}
+
+// ====================================================================
+// Minigame Help Panel
+// ====================================================================
+
+void HUD_MinigameHelp()
+{
+	string help_message;
+	
+	if(!autocvar__hud_configure)
+		help_message = active_minigame.message;
+	else
+		help_message = "Minigame message";
+	
+	if ( !help_message )
+		return;
+	
+	HUD_Panel_UpdateCvars();
+	
+	
+	vector pos, mySize;
+	pos = panel_pos;
+	mySize = panel_size;
+	
+	if(panel_bg_padding)
+	{
+		pos += '1 1 0' * panel_bg_padding;
+		mySize -= '2 2 0' * panel_bg_padding;
+	}
+	
+	minigame_drawcolorcodedstring_wrapped( mySize_x, pos, help_message, 
+		hud_fontsize, panel_fg_alpha, DRAWFLAG_NORMAL, 0.5 );
+}
+
+// ====================================================================
+// Minigame Panel Input
+// ====================================================================
+float HUD_Minigame_InputEvent(float bInputType, float nPrimary, float nSecondary)
+{
+		
+	if( !HUD_MinigameMenu_IsOpened() || autocvar__hud_configure )
+		return false;
+
+	if(bInputType == 3)
+	{
+		mousepos_x = nPrimary;
+		mousepos_y = nSecondary;
+		if ( minigame_isactive() && HUD_mouse_over(HUD_PANEL(MINIGAME_BOARD)) )
+			active_minigame.minigame_event(active_minigame,"mouse_moved",mousepos);
+		return true;
+		
+	}
+	else
+	{
+		
+		if(bInputType == 0) {
+			if(nPrimary == K_ALT) hudShiftState |= S_ALT;
+			if(nPrimary == K_CTRL) hudShiftState |= S_CTRL;
+			if(nPrimary == K_SHIFT) hudShiftState |= S_SHIFT;
+			if(nPrimary == K_MOUSE1) mouseClicked |= S_MOUSE1;
+			if(nPrimary == K_MOUSE2) mouseClicked |= S_MOUSE2;
+		}
+		else if(bInputType == 1) {
+			if(nPrimary == K_ALT) hudShiftState -= (hudShiftState & S_ALT);
+			if(nPrimary == K_CTRL) hudShiftState -= (hudShiftState & S_CTRL);
+			if(nPrimary == K_SHIFT) hudShiftState -= (hudShiftState & S_SHIFT);
+			if(nPrimary == K_MOUSE1) mouseClicked -= (mouseClicked & S_MOUSE1);
+			if(nPrimary == K_MOUSE2) mouseClicked -= (mouseClicked & S_MOUSE2);
+		}
+		
+		// allow some binds
+		string con_keys;
+		float keys;
+		float i;
+		con_keys = findkeysforcommand("toggleconsole", 0);
+		keys = tokenize(con_keys); // findkeysforcommand returns data for this
+		for (i = 0; i < keys; ++i)
+		{
+			if(nPrimary == stof(argv(i)))
+				return false;
+		}
+		
+		if ( minigame_isactive() && ( bInputType == 0 || bInputType == 1 ) )
+		{
+			string device = "";
+			string action = bInputType == 0 ? "pressed" : "released";
+			if ( nPrimary >= K_MOUSE1 && nPrimary <= K_MOUSE16 )
+			{
+				if ( HUD_mouse_over(HUD_PANEL(MINIGAME_BOARD)) )
+					device = "mouse";
+			}
+			else
+				device = "key";
+			
+			if ( device && active_minigame.minigame_event(
+					active_minigame,strcat(device,"_",action),nPrimary) )
+				return true;
+			
+			/// TODO: bInputType == 2?
+		}
+		
+		if ( bInputType == 0 )
+		{
+			if ( nPrimary == K_MOUSE1 && HUD_MinigameMenu_activeitem &&
+				HUD_mouse_over(HUD_PANEL(MINIGAME_MENU)) )
+			{
+				HUD_MinigameMenu_Click(HUD_MinigameMenu_activeitem);
+				return true;
+			}
+			if ( nPrimary == K_UPARROW || nPrimary == K_KP_UPARROW )
+			{
+				if ( HUD_MinigameMenu_activeitem && HUD_MinigameMenu_activeitem.list_prev )
+					HUD_MinigameMenu_activeitem = HUD_MinigameMenu_activeitem.list_prev;
+				else
+					HUD_MinigameMenu_activeitem = HUD_MinigameMenu_last_entry;
+				return true;
+			}
+			else if ( nPrimary == K_DOWNARROW || nPrimary == K_KP_DOWNARROW )
+			{
+				if ( HUD_MinigameMenu_activeitem && HUD_MinigameMenu_activeitem.list_next )
+					HUD_MinigameMenu_activeitem = HUD_MinigameMenu_activeitem.list_next;
+				else
+					HUD_MinigameMenu_activeitem = HUD_MinigameMenu_entries;
+				return true;
+			}
+			else if ( nPrimary == K_HOME || nPrimary == K_KP_HOME )
+			{
+				HUD_MinigameMenu_activeitem = HUD_MinigameMenu_entries;
+				return true;
+			}
+			else if ( nPrimary == K_END || nPrimary == K_KP_END )
+			{
+				HUD_MinigameMenu_activeitem = HUD_MinigameMenu_entries;
+				return true;
+			}
+			else if ( nPrimary == K_KP_ENTER || nPrimary == K_ENTER || nPrimary == K_SPACE )
+			{
+				HUD_MinigameMenu_Click(HUD_MinigameMenu_activeitem);
+				return true;
+			}
+			else if ( nPrimary == K_ESCAPE )
+			{
+				HUD_MinigameMenu_Close();
+				return true;
+			}
+		}
+	}
+	
+	return false;
+
+}
+
+void HUD_Minigame_Mouse()
+{		
+	if( !HUD_MinigameMenu_IsOpened() || autocvar__hud_configure || mv_active )
+		return;
+	
+	if(!autocvar_hud_cursormode)
+	{
+		mousepos = mousepos + getmousepos() * autocvar_menu_mouse_speed;
+
+		mousepos_x = bound(0, mousepos_x, vid_conwidth);
+		mousepos_y = bound(0, mousepos_y, vid_conheight);
+	}
+	
+	if ( HUD_MinigameMenu_IsOpened() && HUD_mouse_over(HUD_PANEL(MINIGAME_MENU)) )
+		HUD_MinigameMenu_MouseInput();
+	
+	vector cursorsize = '32 32 0';
+	drawpic(mousepos-'8 4 0', strcat("gfx/menu/", autocvar_menu_skin, "/cursor.tga"), 
+			cursorsize, '1 1 1', panel_fg_alpha, DRAWFLAG_NORMAL);
+}
+
+float HUD_Minigame_Showpanels()
+{
+	return HUD_MinigameMenu_IsOpened() && ( autocvar__hud_configure || minigame_isactive() );
+}
diff --git a/qcsrc/common/minigames/minigame/all.qh b/qcsrc/common/minigames/minigame/all.qh
new file mode 100644
index 0000000000..e550ff4687
--- /dev/null
+++ b/qcsrc/common/minigames/minigame/all.qh
@@ -0,0 +1,105 @@
+#if defined(SVQC)
+#include "../sv_minigames.qh"
+#elif defined(CSQC)
+#include "../cl_minigames.qh"
+#endif
+
+/**
+
+How to create a minigame
+========================
+
+Create a file for your minigame in this directory and #include it here.
+(ttt.qc implements tic tac toe and can be used as an example)
+and add your minigame to REGISTERED_MINIGAMES (see below)
+
+Required functions
+------------------
+
+SVQC:
+	float minigame_event_<id>(entity minigame, string event, ...count)
+		see ../minigames.qh for a detailed explanation
+CSQC:
+	void minigame_hud_board_<id>(vector pos, vector mySize)
+		draws the main game board inside the rectangle defined by pos and mySize
+		(That rectangle is expressed in window coordinates)
+	void minigame_hud_status_<id>(vector pos, vector mySize)
+		draws the game status panel inside the rectangle defined by pos and mySize
+		(That rectangle is expressed in window coordinates)
+		This panel shows eg scores, captured pieces and so on
+	float minigame_event_<id>(entity minigame, string event, ...count)
+		see ../minigames.qh for a detailed explanation
+
+Managing entities
+-----------------
+
+You can link entities without having to worry about them if their classname
+has been defined in MINIGAME_SIMPLELINKED_ENTITIES (see below)
+Such entities can be spawned with msle_spawn and the system
+will handle networking and cleanup automatically.
+You'll still need to set .SendFlags according to what you specified in FIELD
+in order for them to be sent, ../minigames.qh defines some constants to be
+used as send flags for minigame entities:
+
+* MINIG_SF_CREATE
+	Used when creating a new object, you can use this to define fields that don't change
+* MINIG_SF_UPDATE
+	A miscellaneous update, can be safely used if the entity has just a few fields
+* MINIG_SF_CUSTOM
+	Starting value for custom flags, since there are bit-wise flags, 
+	the following values shall be MINIG_SF_CUSTOM*2, MINIG_SF_CUSTOM*4 and MINIG_SF_CUSTOM*8.
+* MINIG_SF_MAX
+	Maximum flag value that will be networked
+* MINIG_SF_ALL
+	Mask matching all possible flags
+
+Note: As of now, flags are sent as a single byte
+
+Even for non-networked entities, the system provides a system to remove
+automatically unneeded entities when the minigame is over, the requirement is
+that .owner is set to the minigame session entity and .minigame_autoclean is true.
+*/
+
+#include "nmm.qc"
+#include "ttt.qc"
+
+/**
+ * Registration:
+ * 	MINIGAME(id,"Name")
+ * 		id    (QuakeC symbol) Game identifier, used to find the functions explained above
+ * 		"Name"(String)        Human readable name for the game, shown in the UI
+ */
+#define REGISTERED_MINIGAMES \
+	MINIGAME(nmm, "Nine Men's Morris") \
+	MINIGAME(ttt, "Tic Tac Toe") \
+	/*empty line*/
+
+/**
+ * Set up automatic entity read/write functionality
+ * To ensure that everything is handled automatically, spawn on the server using msle_spawn
+ * Syntax:
+ * 	MSLE(classname,Field...) \ 
+ * 		classname: Identifier used to recognize the type of the entity
+ * 		           (must be set as .classname on the sent entities)
+ * 		Field... : List of FIELD calls
+ * 	FIELD(sendflags, Type, field)
+ * 		sendflags: Send flags that signal when this field has to be sent
+ * 		Type     : Type of the entity field. Used to determine WriteX/ReadX functions.
+ * 		           Follows a list of accepted values
+ * 			Byte
+ * 			Char
+ * 			Short
+ * 			Long
+ * 			Coord
+ * 			Angle
+ * 			String	 Note: strzoned on client
+ * 			Float	 Note: implemented as Write/Read Coord
+ * 			Vector	 Note: implemented as Write/Read Coord on _x _y _z
+ * 			Vector2D Note: implemented as Write/Read Coord on _x _y
+ * Note:
+ * 	classname and netname are always sent
+ * 	MSLE stands for Minigame Simple Linked Entity
+ */
+#define MINIGAME_SIMPLELINKED_ENTITIES \
+	MSLE(minigame_board_piece,FIELD(MINIG_SF_CREATE,Byte,team) FIELD(MINIG_SF_UPDATE, Short, minigame_flags) FIELD(MINIG_SF_UPDATE, Vector2D,origin)) \
+	/*empty line*/ 
diff --git a/qcsrc/common/minigames/minigame/nmm.qc b/qcsrc/common/minigames/minigame/nmm.qc
new file mode 100644
index 0000000000..aaebe690ed
--- /dev/null
+++ b/qcsrc/common/minigames/minigame/nmm.qc
@@ -0,0 +1,762 @@
+const float NMM_TURN_PLACE = 0x0100; // player has to place a piece on the board
+const float NMM_TURN_MOVE  = 0x0200; // player has to move a piece by one tile
+const float NMM_TURN_FLY   = 0x0400; // player has to move a piece anywhere
+const float NMM_TURN_TAKE  = 0x0800; // player has to take a non-mill piece
+const float NMM_TURN_TAKEANY=0x1000; // combine with NMM_TURN_TAKE, can take mill pieces
+const float NMM_TURN_WIN   = 0x2000; // player has won
+const float NMM_TURN_TYPE  = 0xff00;
+const float NMM_TURN_TEAM1 = 0x0001;
+const float NMM_TURN_TEAM2 = 0x0002;
+const float NMM_TURN_TEAM  = 0x00ff;
+
+const float NMM_PIECE_DEAD  = 0x0; // captured by the enemy
+const float NMM_PIECE_HOME  = 0x1; // not yet placed
+const float NMM_PIECE_BOARD = 0x2; // placed on the board
+
+.float  nmm_tile_distance;
+.entity nmm_tile_piece;
+.string nmm_tile_hmill;
+.string nmm_tile_vmill;
+
+// build a string containing the indices of the tile to check for a horizontal mill
+string nmm_tile_build_hmill(entity tile)
+{
+	float number = minigame_tile_number(tile.netname);
+	float letter = minigame_tile_letter(tile.netname);
+	if ( number == letter || number+letter == 6 )
+	{
+		float add = letter < 3 ? 1 : -1;
+		return strcat(tile.netname," ",
+			minigame_tile_buildname(letter+add*tile.nmm_tile_distance,number)," ",
+			minigame_tile_buildname(letter+add*2*tile.nmm_tile_distance,number) );
+	}
+	else if ( letter == 3 )
+		return strcat(minigame_tile_buildname(letter-tile.nmm_tile_distance,number)," ",
+			tile.netname," ",
+			minigame_tile_buildname(letter+tile.nmm_tile_distance,number) );
+	else if ( letter < 3 )
+		return strcat(minigame_tile_buildname(0,number)," ",
+			minigame_tile_buildname(1,number)," ",
+			minigame_tile_buildname(2,number) );
+	else
+		return strcat(minigame_tile_buildname(4,number)," ",
+			minigame_tile_buildname(5,number)," ",
+			minigame_tile_buildname(6,number) );
+}
+
+// build a string containing the indices of the tile to check for a vertical mill
+string nmm_tile_build_vmill(entity tile)
+{
+	float letter = minigame_tile_letter(tile.netname);
+	float number = minigame_tile_number(tile.netname);
+	if ( letter == number || letter+number == 6 )
+	{
+		float add = number < 3 ? 1 : -1;
+		return strcat(tile.netname," ",
+			minigame_tile_buildname(letter,number+add*tile.nmm_tile_distance)," ",
+			minigame_tile_buildname(letter,number+add*2*tile.nmm_tile_distance) );
+	}
+	else if ( number == 3 )
+		return strcat(minigame_tile_buildname(letter,number-tile.nmm_tile_distance)," ",
+			tile.netname," ",
+			minigame_tile_buildname(letter,number+tile.nmm_tile_distance) );
+	else if ( number < 3 )
+		return strcat(minigame_tile_buildname(letter,0)," ",
+			minigame_tile_buildname(letter,1)," ",
+			minigame_tile_buildname(letter,2) );
+	else
+		return strcat(minigame_tile_buildname(letter,4)," ",
+			minigame_tile_buildname(letter,5)," ",
+			minigame_tile_buildname(letter,6) );
+}
+
+// Create an new tile
+// \param id       Tile index (eg: a1)
+// \param minig    Owner minigame instance 
+// \param distance Distance from adjacent tiles
+void nmm_spawn_tile(string id, entity minig, float distance)
+{
+	// TODO global variable + list_next for simpler tile loops
+	entity e = spawn();
+	e.origin = minigame_tile_pos(id,7,7);
+	e.classname = "minigame_nmm_tile";
+	e.netname = id;
+	e.owner = minig;
+	e.team = 0;
+	e.nmm_tile_distance = distance;
+	e.nmm_tile_hmill = strzone(nmm_tile_build_hmill(e));
+	e.nmm_tile_vmill = strzone(nmm_tile_build_vmill(e));
+}
+
+// Create a tile square and recursively create inner squares
+// \param minig    Owner minigame instance 
+// \param offset   Index offset (eg: 1 to start the square at b2, 0 at a1 etc.)
+// \param skip     Number of indices to skip between tiles (eg 1: a1, a3)
+void nmm_spawn_tile_square( entity minig, float offset, float skip )
+{
+	float letter = offset;
+	float number = offset;
+	float i, j;
+	for ( i = 0; i < 3; i++ )
+	{
+		number = offset;
+		for ( j = 0; j < 3; j++ )
+		{
+			if ( i != 1 || j != 1 )
+				nmm_spawn_tile(strzone(minigame_tile_buildname(letter,number)),minig, skip+1);
+			number += skip+1;
+		}
+		letter += skip+1;
+	}
+	
+	if ( skip > 0 )
+		nmm_spawn_tile_square(minig,offset+1,skip-1);
+}
+
+// Remove tiles of a NMM minigame
+void nmm_kill_tiles(entity minig)
+{
+	entity e = world;
+	while ( ( e = findentity(e,owner,minig) ) )
+		if ( e.classname == "minigame_nmm_tile" )
+		{
+			strunzone(e.netname);
+			strunzone(e.nmm_tile_hmill);
+			strunzone(e.nmm_tile_vmill);
+			remove(e);
+		}
+}
+
+// Create the tiles of a NMM minigame
+void nmm_init_tiles(entity minig)
+{
+	nmm_spawn_tile_square(minig,0,2);
+}
+
+// Find a tile by its id
+entity nmm_find_tile(entity minig, string id)
+{
+	entity e = world;
+	while ( ( e = findentity(e,owner,minig) ) )
+		if ( e.classname == "minigame_nmm_tile" && e.netname == id )
+			return e;
+	return world;
+}
+
+// Check whether two tiles are adjacent
+float nmm_tile_adjacent(entity tile1, entity tile2)
+{
+		
+	float dnumber = fabs ( minigame_tile_number(tile1.netname) - minigame_tile_number(tile2.netname) );
+	float dletter = fabs ( minigame_tile_letter(tile1.netname) - minigame_tile_letter(tile2.netname) );
+	
+	return ( dnumber == 0 && ( dletter == 1 || dletter == tile1.nmm_tile_distance ) ) ||
+		( dletter == 0 && ( dnumber == 1 || dnumber == tile1.nmm_tile_distance ) );
+}
+
+// Returns 1 if there is at least 1 free adjacent tile
+float nmm_tile_canmove(entity tile)
+{
+	entity e = world;
+	while ( ( e = findentity(e,owner,tile.owner) ) )
+		if ( e.classname == "minigame_nmm_tile" && !e.nmm_tile_piece 
+				&& nmm_tile_adjacent(e,tile) )
+		{
+			return 1;
+		}
+	return 0;
+}
+
+// Check if the given tile id appears in the string
+float nmm_in_mill_string(entity tile, string s)
+{
+	float argc = tokenize(s);
+	float i;
+	for ( i = 0; i < argc; i++ )
+	{
+		entity e = nmm_find_tile(tile.owner,argv(i));
+		if ( !e || !e.nmm_tile_piece || e.nmm_tile_piece.team != tile.nmm_tile_piece.team )
+			return 0;
+	}
+	return 1;
+}
+
+// Check if a tile is in a mill
+float nmm_in_mill(entity tile)
+{
+	return tile.nmm_tile_piece &&  ( 
+		nmm_in_mill_string(tile,tile.nmm_tile_hmill) ||
+		nmm_in_mill_string(tile,tile.nmm_tile_vmill) );
+}
+
+
+#ifdef SVQC
+// Find a NMM piece matching some of the given flags and team number
+entity nmm_find_piece(entity start, entity minigame, float teamn, float pieceflags)
+{
+	entity e = start;
+	while ( ( e = findentity(e,owner,minigame) ) )
+		if ( e.classname == "minigame_board_piece" && 
+				(e.minigame_flags & pieceflags) && e.team == teamn )
+			return e;
+	return world;
+}
+
+// Count NMM pieces matching flags and team number
+float nmm_count_pieces(entity minigame, float teamn, float pieceflags)
+{
+	float n = 0;
+	entity e = world;
+	while (( e = nmm_find_piece(e,minigame, teamn, pieceflags) ))
+		n++;
+	return n;
+}
+
+// required function, handle server side events
+float minigame_event_nmm(entity minigame, string event, ...)
+{
+	if ( event == "start" )
+	{
+		minigame.minigame_flags = NMM_TURN_PLACE|NMM_TURN_TEAM1;
+		nmm_init_tiles(minigame);
+		float i;
+		entity e;
+		for ( i = 0; i < 7; i++ )
+		{
+			e = msle_spawn(minigame,"minigame_board_piece");
+			e.team = 1;
+			e.minigame_flags = NMM_PIECE_HOME;
+			e = msle_spawn(minigame,"minigame_board_piece");
+			e.team = 2;
+			e.minigame_flags = NMM_PIECE_HOME;
+		}
+			
+		return 1;
+	}
+	else if ( event == "end" )
+	{
+		nmm_kill_tiles(minigame);
+	}
+	else if ( event == "join" )
+	{
+		float n = 0;
+		entity e;
+		for ( e = minigame.minigame_players; e; e = e.list_next )
+			n++;
+		if ( n >= 2 )
+			return 0;
+		if ( minigame.minigame_players && minigame.minigame_players.team == 1 )
+			return 2;
+		return 1;
+	}
+	else if ( event == "cmd" )
+	{
+		entity e = ...(0,entity);
+		float argc = ...(1,float);
+		entity tile = world;
+		entity piece = world;
+		float move_ok = 0;
+		
+		if ( e && argc >= 2 && argv(0) == "move" && 
+			( minigame.minigame_flags & NMM_TURN_TEAM ) == e.team )
+		{
+			tile = nmm_find_tile(minigame,argv(1));
+			if ( !tile )
+			{
+				move_ok = 0;
+			}
+			else if ( minigame.minigame_flags & NMM_TURN_PLACE )
+			{
+				piece = nmm_find_piece(world,minigame,e.team,NMM_PIECE_HOME);
+				if ( !tile.nmm_tile_piece && piece )
+				{
+					tile.nmm_tile_piece = piece;
+					piece.minigame_flags = NMM_PIECE_BOARD;
+					piece.origin = tile.origin;
+					piece.SendFlags |= MINIG_SF_UPDATE;
+					move_ok = 1;
+				}
+			}
+			else if ( minigame.minigame_flags & NMM_TURN_MOVE )
+			{
+				if ( tile.nmm_tile_piece && tile.nmm_tile_piece.team == e.team )
+				{
+					piece = tile.nmm_tile_piece;
+					entity tile2 = nmm_find_tile(minigame,argv(2));
+					if ( tile2 && nmm_tile_adjacent(tile,tile2) && !tile2.nmm_tile_piece )
+					{
+						tile.nmm_tile_piece = world;
+						tile2.nmm_tile_piece = piece;
+						piece.origin = tile2.origin;
+						piece.SendFlags |= MINIG_SF_UPDATE;
+						tile = tile2;
+						move_ok = 1;
+					}
+				}
+				
+			}
+			else if ( minigame.minigame_flags & NMM_TURN_FLY )
+			{
+				if ( tile.nmm_tile_piece && tile.nmm_tile_piece.team == e.team )
+				{
+					piece = tile.nmm_tile_piece;
+					entity tile2 = nmm_find_tile(minigame,argv(2));
+					if ( tile2 && !tile2.nmm_tile_piece )
+					{
+						tile.nmm_tile_piece = world;
+						tile2.nmm_tile_piece = piece;
+						piece.origin = tile2.origin;
+						piece.SendFlags |= MINIG_SF_UPDATE;
+						tile = tile2;
+						move_ok = 1;
+					}
+				}
+				
+			}
+			else if ( minigame.minigame_flags & NMM_TURN_TAKE )
+			{
+				piece = tile.nmm_tile_piece;
+				if ( piece && piece.nmm_tile_piece.team != e.team )
+				{
+					tile.nmm_tile_piece = world;
+					piece.minigame_flags = NMM_PIECE_DEAD;
+					piece.SendFlags |= MINIG_SF_UPDATE;
+					move_ok = 1;
+				}
+			}
+			
+			float nextteam = e.team % 2 + 1;
+			float npieces = nmm_count_pieces(minigame,nextteam,NMM_PIECE_HOME|NMM_PIECE_BOARD);
+			
+			if ( npieces < 3 )
+			{
+				minigame.minigame_flags = NMM_TURN_WIN | e.team;
+				minigame.SendFlags |= MINIG_SF_UPDATE;
+			}
+			else if ( move_ok)
+			{
+				if ( !(minigame.minigame_flags & NMM_TURN_TAKE) && nmm_in_mill(tile) )
+				{
+					minigame.minigame_flags = NMM_TURN_TAKE|e.team;
+					float takemill = NMM_TURN_TAKEANY;
+					entity f = world;
+					while ( ( f = findentity(f,owner,minigame) ) )
+						if ( f.classname == "minigame_nmm_tile" && f.nmm_tile_piece  &&
+								f.nmm_tile_piece.team == nextteam && !nmm_in_mill(f) )
+						{
+							takemill = 0;
+							break;
+						}
+					minigame.minigame_flags |= takemill;
+				}
+				else
+				{
+					if ( nmm_find_piece(world,minigame,nextteam,NMM_PIECE_HOME) )
+						minigame.minigame_flags = NMM_TURN_PLACE|nextteam;
+					else if ( npieces == 3 )
+						minigame.minigame_flags = NMM_TURN_FLY|nextteam;
+					else
+					{
+						minigame.minigame_flags = NMM_TURN_WIN|e.team;
+						entity f = world;
+						while ( ( f = findentity(f,owner,minigame) ) )
+							if ( f.classname == "minigame_nmm_tile" && f.nmm_tile_piece  &&
+								f.nmm_tile_piece.team == nextteam && nmm_tile_canmove(f) )
+							{
+								minigame.minigame_flags = NMM_TURN_MOVE|nextteam;
+								break;
+							}
+					}
+				}
+				minigame.SendFlags |= MINIG_SF_UPDATE;
+			}
+			else
+				dprint("Invalid move: ",...(2,string),"\n");
+			return 1;
+		}
+	}
+	return 0;
+}
+
+#elif defined(CSQC)
+
+entity nmm_currtile;
+entity nmm_fromtile;
+
+vector nmm_boardpos;
+vector nmm_boardsize;
+
+// whether the given tile is a valid selection
+float nmm_valid_selection(entity tile)
+{
+	if ( ( tile.owner.minigame_flags & NMM_TURN_TEAM ) != minigame_self.team )
+		return 0; // not our turn
+	if ( tile.owner.minigame_flags & NMM_TURN_PLACE )
+		return !tile.nmm_tile_piece; // need to put a piece on an empty spot
+	if ( tile.owner.minigame_flags & NMM_TURN_MOVE )
+	{
+		if ( tile.nmm_tile_piece && tile.nmm_tile_piece.team == minigame_self.team &&
+				nmm_tile_canmove(tile) )
+			return 1; //  movable tile
+		if ( nmm_fromtile ) // valid destination
+			return !tile.nmm_tile_piece && nmm_tile_adjacent(nmm_fromtile,tile);
+		return 0;
+	}
+	if ( tile.owner.minigame_flags & NMM_TURN_FLY )
+	{
+		if ( nmm_fromtile )
+			return !tile.nmm_tile_piece;
+		else
+			return tile.nmm_tile_piece && tile.nmm_tile_piece.team == minigame_self.team;
+	}
+	if ( tile.owner.minigame_flags & NMM_TURN_TAKE )
+		return tile.nmm_tile_piece && tile.nmm_tile_piece.team != minigame_self.team &&
+			( (tile.owner.minigame_flags & NMM_TURN_TAKEANY) || !nmm_in_mill(tile) );
+	return 0;
+}
+
+// whether it should highlight valid tile selections
+float nmm_draw_avaliable(entity tile)
+{
+	if ( ( tile.owner.minigame_flags & NMM_TURN_TEAM ) != minigame_self.team )
+		return 0;
+	if ( (tile.owner.minigame_flags & NMM_TURN_TAKE) )
+		return 1;
+	if ( (tile.owner.minigame_flags & (NMM_TURN_FLY|NMM_TURN_MOVE)) && nmm_fromtile )
+		return !tile.nmm_tile_piece;
+	return 0;
+}
+
+// Required function, draw the game board
+void minigame_hud_board_nmm(vector pos, vector mySize)
+{
+	minigame_hud_fitsqare(pos, mySize);
+	nmm_boardpos = pos;
+	nmm_boardsize = mySize;
+	minigame_hud_simpleboard(pos,mySize,minigame_texture("nmm/board"));
+	
+	vector tile_size = minigame_hud_denormalize_size('1 1 0'/7,pos,mySize);
+	vector tile_pos;
+	entity e;
+	FOREACH_MINIGAME_ENTITY(e)
+	{
+		if ( e.classname == "minigame_nmm_tile" )
+		{
+			tile_pos = minigame_hud_denormalize(e.origin,pos,mySize);
+			
+			if ( e == nmm_fromtile )
+			{
+				minigame_drawpic_centered( tile_pos, minigame_texture("nmm/tile_active"),
+					tile_size, '1 1 1', panel_fg_alpha, DRAWFLAG_NORMAL );
+			}
+			else if ( nmm_draw_avaliable(e) && nmm_valid_selection(e) )
+			{
+				minigame_drawpic_centered( tile_pos, minigame_texture("nmm/tile_available"),
+					tile_size, '1 1 1', panel_fg_alpha, DRAWFLAG_NORMAL );
+			}
+			
+			if ( e == nmm_currtile )
+			{
+				minigame_drawpic_centered( tile_pos, minigame_texture("nmm/tile_selected"),
+					tile_size, '1 1 1', panel_fg_alpha, DRAWFLAG_ADDITIVE );
+			}
+			
+			if ( e.nmm_tile_piece )
+			{
+				minigame_drawpic_centered( tile_pos,  
+					minigame_texture(strcat("nmm/piece",ftos(e.nmm_tile_piece.team))),
+					tile_size*0.8, '1 1 1', panel_fg_alpha, DRAWFLAG_NORMAL );
+			}
+			
+			//drawstring(tile_pos, e.netname, hud_fontsize, '1 0 0', 1, DRAWFLAG_NORMAL);
+		}
+	}
+	
+	if ( active_minigame.minigame_flags & NMM_TURN_WIN )
+	{
+		vector winfs = hud_fontsize*2;
+		string playername = "";
+		FOREACH_MINIGAME_ENTITY(e)
+			if ( e.classname == "minigame_player" && 
+					e.team == (active_minigame.minigame_flags & NMM_TURN_TEAM) )
+				playername = GetPlayerName(e.minigame_playerslot-1);
+		
+		vector win_pos = pos+eY*(mySize_y-winfs_y)/2;
+		vector win_sz;
+		win_sz = minigame_drawcolorcodedstring_wrapped(mySize_x,win_pos,
+			sprintf("%s^7 won the game!",playername), 
+			winfs, 0, DRAWFLAG_NORMAL, 0.5);
+		
+		drawfill(win_pos-eY*hud_fontsize_y,win_sz+2*eY*hud_fontsize_y,'1 1 1',0.5,DRAWFLAG_ADDITIVE);
+		
+		minigame_drawcolorcodedstring_wrapped(mySize_x,win_pos,
+			sprintf("%s^7 won the game!",playername), 
+			winfs, panel_fg_alpha, DRAWFLAG_NORMAL, 0.5);
+	}
+}
+
+// Required function, draw the game status panel
+void minigame_hud_status_nmm(vector pos, vector mySize)
+{
+	HUD_Panel_DrawBg(1);
+	vector ts;
+	
+	ts = minigame_drawstring_wrapped(mySize_x,pos,active_minigame.descriptor.message,
+		hud_fontsize * 2, '0.25 0.47 0.72', panel_fg_alpha, DRAWFLAG_NORMAL,0.5);
+	pos_y += ts_y;
+	mySize_y -= ts_y;
+	
+	vector player_fontsize = hud_fontsize * 1.75;
+	ts_y = ( mySize_y - 2*player_fontsize_y ) / 2;
+	ts_x = mySize_x;
+	
+	float player1x = 0;
+	float player2x = 0;
+	vector piece_sz = '48 48 0';
+	float piece_space = piece_sz_x + ( ts_x - 7 * piece_sz_x ) / 6;
+	vector mypos;
+	float piece_light = 1;
+	entity e = world;
+	
+	mypos = pos;
+	if ( (active_minigame.minigame_flags&NMM_TURN_TEAM) == 2 )
+		mypos_y  += player_fontsize_y + ts_y;
+	drawfill(mypos,eX*mySize_x+eY*player_fontsize_y,'1 1 1',0.5,DRAWFLAG_ADDITIVE);
+	mypos_y += player_fontsize_y;
+	drawfill(mypos,eX*mySize_x+eY*piece_sz_y,'1 1 1',0.25,DRAWFLAG_ADDITIVE);
+	
+	FOREACH_MINIGAME_ENTITY(e)
+	{
+		if ( e.classname == "minigame_player" )
+		{
+			mypos = pos;
+			if ( e.team == 2 )
+				mypos_y  += player_fontsize_y + ts_y;
+			minigame_drawcolorcodedstring_trunc(mySize_x,mypos,
+				GetPlayerName(e.minigame_playerslot-1),
+				player_fontsize, panel_fg_alpha, DRAWFLAG_NORMAL);
+		}
+		else if ( e.classname == "minigame_board_piece" )
+		{
+			mypos = pos;
+			mypos_y += player_fontsize_y;
+			if ( e.team == 2 )
+			{
+				mypos_x += player2x;
+				player2x += piece_space;
+				mypos_y  += player_fontsize_y + ts_y;
+			}
+			else
+			{
+				mypos_x += player1x;
+				player1x += piece_space;
+			}
+			if ( e.minigame_flags == NMM_PIECE_HOME )
+				piece_light = 0.5;
+			else if ( e.minigame_flags == NMM_PIECE_BOARD )
+				piece_light = 1;
+			else
+				piece_light = 0.15;
+			
+			drawpic(mypos, minigame_texture(strcat("nmm/piece",ftos(e.team))), piece_sz,
+				'1 1 1'*piece_light, panel_fg_alpha, DRAWFLAG_NORMAL );
+		}
+	}
+}
+
+// Make the correct move
+void nmm_make_move(entity minigame)
+{
+	if ( nmm_currtile )
+	{
+		if ( minigame.minigame_flags & (NMM_TURN_PLACE|NMM_TURN_TAKE) )
+		{
+			minigame_cmd("move ",nmm_currtile.netname);
+			nmm_fromtile = world;
+		}
+		else if ( (minigame.minigame_flags & (NMM_TURN_MOVE|NMM_TURN_FLY)) )
+		{
+			if ( nmm_fromtile == nmm_currtile )
+			{
+				nmm_fromtile = world;
+			}
+			else if ( nmm_currtile.nmm_tile_piece && nmm_currtile.nmm_tile_piece.team == minigame_self.team )
+			{
+				nmm_fromtile = nmm_currtile;
+			}
+			else if ( nmm_fromtile )
+			{
+				minigame_cmd("move ",nmm_fromtile.netname," ",nmm_currtile.netname);
+				nmm_fromtile = world;
+			}
+		}
+	}
+	else
+		nmm_fromtile = world;
+}
+
+string nmm_turn_to_string(float turnflags)
+{
+	if ( turnflags & NMM_TURN_WIN )
+	{
+		if ( (turnflags&NMM_TURN_TEAM) != minigame_self.team )
+			return _("You lost the game!");
+		return _("You win!");
+	}
+	
+	if ( (turnflags&NMM_TURN_TEAM) != minigame_self.team )
+		return _("Wait for your opponent to make their move");
+	if ( turnflags & NMM_TURN_PLACE )
+		return _("Click on the game board to place your piece");
+	if ( turnflags & NMM_TURN_MOVE )
+		return _("You can select one of your pieces to move it in one of the surrounding places");
+	if ( turnflags & NMM_TURN_FLY )
+		return _("You can select one of your pieces to move it anywhere on the board");
+	if ( turnflags & NMM_TURN_TAKE )
+		return _("You can take one of the opponent's pieces");
+	
+	return "";
+}
+
+// Required function, handle client events
+float minigame_event_nmm(entity minigame, string event, ...)
+{
+	if ( event == "activate" )
+	{
+		nmm_fromtile = world;
+		nmm_init_tiles(minigame);
+		minigame.message = nmm_turn_to_string(minigame.minigame_flags);
+	}
+	else if ( event == "deactivate" )
+	{
+		nmm_fromtile = world;
+		nmm_kill_tiles(minigame);
+	}
+	else if ( event == "key_pressed" && (minigame.minigame_flags&NMM_TURN_TEAM) == minigame_self.team )
+	{
+		switch ( ...(0,float) )
+		{
+			case K_RIGHTARROW:
+			case K_KP_RIGHTARROW:
+				if ( ! nmm_currtile )
+					nmm_currtile = nmm_find_tile(active_minigame,"a7");
+				else
+				{
+					string tileid = nmm_currtile.netname;
+					nmm_currtile = world; 
+					while ( !nmm_currtile )
+					{
+						tileid = minigame_relative_tile(tileid,1,0,7,7);
+						nmm_currtile = nmm_find_tile(active_minigame,tileid);
+					}
+				}
+				return 1;
+			case K_LEFTARROW:
+			case K_KP_LEFTARROW:
+				if ( ! nmm_currtile )
+					nmm_currtile = nmm_find_tile(active_minigame,"g7");
+				else
+				{
+					string tileid = nmm_currtile.netname;
+					nmm_currtile = world; 
+					while ( !nmm_currtile )
+					{
+						tileid = minigame_relative_tile(tileid,-1,0,7,7);
+						nmm_currtile = nmm_find_tile(active_minigame,tileid);
+					}
+				}
+				return 1;
+			case K_UPARROW:
+			case K_KP_UPARROW:
+				if ( ! nmm_currtile )
+					nmm_currtile = nmm_find_tile(active_minigame,"a1");
+				else
+				{
+					string tileid = nmm_currtile.netname;
+					nmm_currtile = world; 
+					while ( !nmm_currtile )
+					{
+						tileid = minigame_relative_tile(tileid,0,1,7,7);
+						nmm_currtile = nmm_find_tile(active_minigame,tileid);
+					}
+				}
+				return 1;
+			case K_DOWNARROW:
+			case K_KP_DOWNARROW:
+				if ( ! nmm_currtile )
+					nmm_currtile = nmm_find_tile(active_minigame,"a7");
+				else
+				{
+					string tileid = nmm_currtile.netname;
+					nmm_currtile = world; 
+					while ( !nmm_currtile )
+					{
+						tileid = minigame_relative_tile(tileid,0,-1,7,7);
+						nmm_currtile = nmm_find_tile(active_minigame,tileid);
+					}
+				}
+				return 1;
+			case K_ENTER:
+			case K_KP_ENTER:
+			case K_SPACE:
+				nmm_make_move(minigame);
+				return 1;
+		}
+		return 0;
+	}
+	else if ( event == "mouse_pressed" && ...(0,float) == K_MOUSE1 )
+	{
+		nmm_make_move(minigame);
+		return 1;
+	}
+	else if ( event == "mouse_moved" )
+	{
+		nmm_currtile = world;
+		vector tile_pos;
+		vector tile_size = minigame_hud_denormalize_size('1 1 0'/7,nmm_boardpos,nmm_boardsize);
+		entity e;
+		FOREACH_MINIGAME_ENTITY(e)
+		{
+			if ( e.classname == "minigame_nmm_tile" )
+			{
+				tile_pos = minigame_hud_denormalize(e.origin,nmm_boardpos,nmm_boardsize)-tile_size/2;
+				if ( minigame_hud_mouse_in(tile_pos, tile_size) && nmm_valid_selection(e) )
+				{
+					nmm_currtile = e;
+					break;
+				}
+			}
+		}
+		return 1;
+	}
+	else if ( event == "network_receive" )
+	{
+		if ( self.classname == "minigame_board_piece" && ( ...(1,float) & MINIG_SF_UPDATE ) )
+		{
+			entity e;
+			string tileid = "";
+			if ( self.minigame_flags & NMM_PIECE_BOARD )
+				tileid = minigame_tile_name(self.origin,7,7);
+			FOREACH_MINIGAME_ENTITY(e)
+			{
+				if ( e.classname == "minigame_nmm_tile" )
+				{
+					if ( e.nmm_tile_piece == self )
+						e.nmm_tile_piece = world;
+					if ( e.netname == tileid )
+						e.nmm_tile_piece = self;
+				}
+			}
+		}
+		else if ( self.classname == "minigame" && ( ...(1,float) & MINIG_SF_UPDATE ) ) 
+		{
+			self.message = nmm_turn_to_string(self.minigame_flags);
+			if ( self.minigame_flags & minigame_self.team )
+				minigame_prompt();
+		}
+	}
+	
+	return 0;
+}
+
+#endif 
diff --git a/qcsrc/common/minigames/minigame/ttt.qc b/qcsrc/common/minigames/minigame/ttt.qc
new file mode 100644
index 0000000000..e2dc0a06af
--- /dev/null
+++ b/qcsrc/common/minigames/minigame/ttt.qc
@@ -0,0 +1,688 @@
+const float TTT_TURN_PLACE = 0x0100; // player has to place a piece on the board
+const float TTT_TURN_WIN   = 0x0200; // player has won
+const float TTT_TURN_DRAW  = 0x0400; // no moves are possible
+const float TTT_TURN_NEXT  = 0x0800; // a player wants to start a new match
+const float TTT_TURN_TYPE  = 0x0f00; // turn type mask
+
+const float TTT_TURN_TEAM1 = 0x0001;
+const float TTT_TURN_TEAM2 = 0x0002;
+const float TTT_TURN_TEAM  = 0x000f; // turn team mask
+
+// send flags
+const float TTT_SF_PLAYERSCORE  = MINIG_SF_CUSTOM;   // send minigame_player scores (won matches)
+const float TTT_SF_SINGLEPLAYER = MINIG_SF_CUSTOM<<1;// send minigame.ttt_ai
+
+.float ttt_npieces; // (minigame) number of pieces on the board (simplifies checking a draw)
+.float ttt_nexteam; // (minigame) next team (used to change the starting team on following matches)
+.float ttt_ai;      // (minigame) when non-zero, singleplayer vs AI
+
+// find tic tac toe piece given its tile name
+entity ttt_find_piece(entity minig, string tile)
+{
+	entity e = world;
+	while ( ( e = findentity(e,owner,minig) ) )
+		if ( e.classname == "minigame_board_piece" && e.netname == tile )
+			return e;
+	return world;
+}
+
+// Checks if the given piece completes a row
+float ttt_winning_piece(entity piece)
+{
+	float number = minigame_tile_number(piece.netname);
+	float letter = minigame_tile_letter(piece.netname);
+	
+	if ( ttt_find_piece(piece.owner,minigame_tile_buildname(0,number)).team == piece.team )
+	if ( ttt_find_piece(piece.owner,minigame_tile_buildname(1,number)).team == piece.team )
+	if ( ttt_find_piece(piece.owner,minigame_tile_buildname(2,number)).team == piece.team )
+		return 1;
+	
+	if ( ttt_find_piece(piece.owner,minigame_tile_buildname(letter,0)).team == piece.team )
+	if ( ttt_find_piece(piece.owner,minigame_tile_buildname(letter,1)).team == piece.team )
+	if ( ttt_find_piece(piece.owner,minigame_tile_buildname(letter,2)).team == piece.team )
+		return 1;
+	
+	if ( number == letter )
+	if ( ttt_find_piece(piece.owner,minigame_tile_buildname(0,0)).team == piece.team )
+	if ( ttt_find_piece(piece.owner,minigame_tile_buildname(1,1)).team == piece.team )
+	if ( ttt_find_piece(piece.owner,minigame_tile_buildname(2,2)).team == piece.team )
+		return 1;
+	
+	if ( number == 2-letter )
+	if ( ttt_find_piece(piece.owner,minigame_tile_buildname(0,2)).team == piece.team )
+	if ( ttt_find_piece(piece.owner,minigame_tile_buildname(1,1)).team == piece.team )
+	if ( ttt_find_piece(piece.owner,minigame_tile_buildname(2,0)).team == piece.team )
+		return 1;
+	
+	return 0;
+}
+
+// check if the tile name is valid (3x3 grid)
+float ttt_valid_tile(string tile)
+{
+	if ( !tile )
+		return 0;
+	float number = minigame_tile_number(tile);
+	float letter = minigame_tile_letter(tile);
+	return 0 <= number && number < 3 && 0 <= letter && letter < 3;
+}
+
+// make a move
+void ttt_move(entity minigame, entity player, string pos )
+{
+	if ( minigame.minigame_flags & TTT_TURN_PLACE )
+	if ( pos && player.team == (minigame.minigame_flags & TTT_TURN_TEAM) )
+	{
+		if ( ttt_valid_tile(pos) )
+		if ( !ttt_find_piece(minigame,pos) )
+		{
+			entity piece = msle_spawn(minigame,"minigame_board_piece");
+			piece.team = player.team;
+			piece.netname = strzone(pos);
+			minigame_server_sendflags(piece,MINIG_SF_ALL);
+			minigame_server_sendflags(minigame,MINIG_SF_UPDATE);
+			minigame.ttt_npieces++;
+			minigame.ttt_nexteam = minigame_next_team(player.team,2);
+			if ( ttt_winning_piece(piece) )
+			{
+				player.minigame_flags++;
+				minigame_server_sendflags(player, TTT_SF_PLAYERSCORE);
+				minigame.minigame_flags = TTT_TURN_WIN | player.team;
+			}
+			else if ( minigame.ttt_npieces >= 9 )
+				minigame.minigame_flags = TTT_TURN_DRAW;
+			else
+				minigame.minigame_flags = TTT_TURN_PLACE | minigame.ttt_nexteam;
+		}
+	}
+}
+
+// request a new match
+void ttt_next_match(entity minigame, entity player)
+{
+#ifdef SVQC
+	// on multiplayer matches, wait for both players to agree
+	if ( minigame.minigame_flags & (TTT_TURN_WIN|TTT_TURN_DRAW) )
+	{
+		minigame.minigame_flags = TTT_TURN_NEXT | player.team;
+		minigame.SendFlags |= MINIG_SF_UPDATE;
+	}
+	else if ( (minigame.minigame_flags & TTT_TURN_NEXT) &&
+			!( minigame.minigame_flags & player.team ) )
+#endif
+	{
+		minigame.minigame_flags = TTT_TURN_PLACE | minigame.ttt_nexteam;
+		minigame_server_sendflags(minigame,MINIG_SF_UPDATE);
+		minigame.ttt_npieces = 0;
+		entity e = world;
+		while ( ( e = findentity(e,owner,minigame) ) )
+			if ( e.classname == "minigame_board_piece" )
+				remove(e);
+	}
+}
+
+#ifdef SVQC
+
+
+// required function, handle server side events
+float minigame_event_ttt(entity minigame, string event, ...)
+{
+	switch(event)
+	{
+		case "start":
+		{
+			minigame.minigame_flags = (TTT_TURN_PLACE | TTT_TURN_TEAM1);
+			return true;
+		}
+		case "end":
+		{
+			entity e = world;
+			while( (e = findentity(e, owner, minigame)) )
+			if(e.classname == "minigame_board_piece")
+			{
+				if(e.netname) { strunzone(e.netname); }
+				remove(e);
+			}
+			return false;
+		}
+		case "join":
+		{
+			float pl_num = minigame_count_players(minigame);
+			
+			// Don't allow joining a single player match
+			if ( (minigame.ttt_ai) && pl_num > 0 )
+				return false;
+
+			// Don't allow more than 2 players
+			if(pl_num >= 2) { return false; }
+
+			// Get the right team
+			if(minigame.minigame_players)
+				return minigame_next_team(minigame.minigame_players.team, 2);
+
+			// Team 1 by default
+			return 1;
+		}
+		case "cmd":
+		{
+			switch(argv(0))
+			{
+				case "move": 
+					ttt_move(minigame, ...(0,entity), ...(1,float) == 2 ? argv(1) : string_null ); 
+					return true;
+				case "next":
+					ttt_next_match(minigame,...(0,entity));
+					return true;
+				case "singleplayer":
+					if ( minigame_count_players(minigame) == 1 )
+					{
+						minigame.ttt_ai = minigame_next_team(minigame.minigame_players.team, 2);
+						minigame.SendFlags = TTT_SF_SINGLEPLAYER;
+					}
+					return true;
+			}
+
+			return false;
+		}
+		case "network_send":
+		{
+			entity sent = ...(0,entity);
+			float sf = ...(1,float);
+			if ( sent.classname == "minigame_player" && (sf & TTT_SF_PLAYERSCORE ) )
+			{
+				WriteByte(MSG_ENTITY,sent.minigame_flags);
+			}
+			else if ( sent.classname == "minigame" && (sf & TTT_SF_SINGLEPLAYER) )
+			{
+				WriteByte(MSG_ENTITY,sent.ttt_ai);
+			}
+			return false;
+		}
+	}
+	
+	return false;
+}
+
+
+#elif defined(CSQC)
+
+string ttt_curr_pos; // identifier of the tile under the mouse
+vector ttt_boardpos; // HUD board position
+vector ttt_boardsize;// HUD board size
+.float ttt_checkwin; // Used to optimize checks to display a win
+
+// Required function, draw the game board
+void minigame_hud_board_ttt(vector pos, vector mySize)
+{
+	minigame_hud_fitsqare(pos, mySize);
+	ttt_boardpos = pos;
+	ttt_boardsize = mySize;
+	
+	minigame_hud_simpleboard(pos,mySize,minigame_texture("ttt/board"));
+
+	vector tile_size = minigame_hud_denormalize_size('1 1 0'/3,pos,mySize);
+	vector tile_pos;
+
+	if ( (active_minigame.minigame_flags & TTT_TURN_TEAM) == minigame_self.team )
+	if ( ttt_valid_tile(ttt_curr_pos) )
+	{
+		tile_pos = minigame_tile_pos(ttt_curr_pos,3,3);
+		tile_pos = minigame_hud_denormalize(tile_pos,pos,mySize);
+		minigame_drawpic_centered( tile_pos,  
+				minigame_texture(strcat("ttt/piece",ftos(minigame_self.team))),
+				tile_size, '1 1 1', panel_fg_alpha/2, DRAWFLAG_NORMAL );
+	}
+	
+	entity e;
+	FOREACH_MINIGAME_ENTITY(e)
+	{
+		if ( e.classname == "minigame_board_piece" )
+		{
+			tile_pos = minigame_tile_pos(e.netname,3,3);
+			tile_pos = minigame_hud_denormalize(tile_pos,pos,mySize);
+			
+			if ( active_minigame.minigame_flags & TTT_TURN_WIN )
+			if ( !e.ttt_checkwin )
+				e.ttt_checkwin = ttt_winning_piece(e) ? 1 : -1;
+			
+			float icon_color = 1;
+			if ( e.ttt_checkwin == -1 )
+				icon_color = 0.4;
+			else if ( e.ttt_checkwin == 1 )
+			{
+				icon_color = 2;
+				minigame_drawpic_centered( tile_pos, minigame_texture("ttt/winglow"),
+						tile_size, '1 1 1', panel_fg_alpha, DRAWFLAG_ADDITIVE );
+			}
+				
+			minigame_drawpic_centered( tile_pos,  
+					minigame_texture(strcat("ttt/piece",ftos(e.team))),
+					tile_size, '1 1 1'*icon_color, panel_fg_alpha, DRAWFLAG_NORMAL );
+		}
+	}
+}
+
+
+// Required function, draw the game status panel
+void minigame_hud_status_ttt(vector pos, vector mySize)
+{
+	HUD_Panel_DrawBg(1);
+	vector ts;
+	ts = minigame_drawstring_wrapped(mySize_x,pos,active_minigame.descriptor.message,
+		hud_fontsize * 2, '0.25 0.47 0.72', panel_fg_alpha, DRAWFLAG_NORMAL,0.5);
+	
+	pos_y += ts_y;
+	mySize_y -= ts_y;
+	
+	vector player_fontsize = hud_fontsize * 1.75;
+	ts_y = ( mySize_y - 2*player_fontsize_y ) / 2;
+	ts_x = mySize_x;
+	vector mypos;
+	vector tile_size = '48 48 0';
+
+	entity e;
+	FOREACH_MINIGAME_ENTITY(e)
+	{
+		if ( e.classname == "minigame_player" )
+		{
+			mypos = pos;
+			if ( e.team == 2 )
+				mypos_y  += player_fontsize_y + ts_y;
+			minigame_drawcolorcodedstring_trunc(mySize_x,mypos,
+				(e.minigame_playerslot ? GetPlayerName(e.minigame_playerslot-1) : _("AI")),
+				player_fontsize, panel_fg_alpha, DRAWFLAG_NORMAL);
+			
+			mypos_y += player_fontsize_y;
+			drawpic( mypos,  
+					minigame_texture(strcat("ttt/piece",ftos(e.team))),
+					tile_size, '1 1 1', panel_fg_alpha, DRAWFLAG_NORMAL );
+			
+			mypos_x += tile_size_x;
+			
+			drawstring(mypos,ftos(e.minigame_flags),tile_size,
+					   '0.7 0.84 1', panel_fg_alpha, DRAWFLAG_NORMAL);
+		}
+	}
+}
+
+// Turn a set of flags into a help message
+string ttt_turn_to_string(float turnflags)
+{
+	if ( turnflags & TTT_TURN_DRAW )
+		return _("Draw");
+	
+	if ( turnflags & TTT_TURN_WIN )
+	{
+		if ( (turnflags&TTT_TURN_TEAM) != minigame_self.team )
+			return _("You lost the game!\nSelect \"^1Next Match^7\" on the menu for a rematch!");
+		return _("You win!\nSelect \"^1Next Match^7\" on the menu to start a new match!");
+	}
+	
+	if ( turnflags & TTT_TURN_NEXT )
+	{
+		if ( (turnflags&TTT_TURN_TEAM) != minigame_self.team )
+			return _("Select \"^1Next Match^7\" on the menu to start a new match!");
+		return _("Wait for your opponent to confirm the rematch");
+	}
+	
+	if ( (turnflags & TTT_TURN_TEAM) != minigame_self.team )
+		return _("Wait for your opponent to make their move");
+	
+	if ( turnflags & TTT_TURN_PLACE )
+		return _("Click on the game board to place your piece");
+	
+	return "";
+}
+
+const float TTT_AI_POSFLAG_A1 = 0x0001;
+const float TTT_AI_POSFLAG_A2 = 0x0002;
+const float TTT_AI_POSFLAG_A3 = 0x0004;
+const float TTT_AI_POSFLAG_B1 = 0x0008;
+const float TTT_AI_POSFLAG_B2 = 0x0010;
+const float TTT_AI_POSFLAG_B3 = 0x0020;
+const float TTT_AI_POSFLAG_C1 = 0x0040;
+const float TTT_AI_POSFLAG_C2 = 0x0080;
+const float TTT_AI_POSFLAG_C3 = 0x0100;
+
+// convert a flag to a position
+string ttt_ai_piece_flag2pos(float pieceflag)
+{
+	switch(pieceflag)
+	{
+		case TTT_AI_POSFLAG_A1:
+			return "a1";
+		case TTT_AI_POSFLAG_A2:
+			return "a2";
+		case TTT_AI_POSFLAG_A3:
+			return "a3";
+			
+		case TTT_AI_POSFLAG_B1:
+			return "b1";
+		case TTT_AI_POSFLAG_B2:
+			return "b2";
+		case TTT_AI_POSFLAG_B3:
+			return "b3";
+			
+		case TTT_AI_POSFLAG_C1:
+			return "c1";
+		case TTT_AI_POSFLAG_C2:
+			return "c2";
+		case TTT_AI_POSFLAG_C3:
+			return "c3";
+			
+		default:
+			return string_null;
+	}
+}
+
+float ttt_ai_checkmask(float piecemask, float checkflags)
+{
+	return checkflags && (piecemask & checkflags) == checkflags;
+}
+
+// get the third flag if the mask matches two of them
+float ttt_ai_1of3(float piecemask, float flag1, float flag2, float flag3)
+{
+	if ( ttt_ai_checkmask(piecemask,flag1|flag2|flag3) )
+		return 0;
+	
+	if ( ttt_ai_checkmask(piecemask,flag1|flag2) )
+		return flag3;
+	
+	if ( ttt_ai_checkmask(piecemask,flag3|flag2) )
+		return flag1;
+	
+	if ( ttt_ai_checkmask(piecemask,flag3|flag1) )
+		return flag2;
+
+	return 0;
+}
+
+// Select a random flag in the mask
+float ttt_ai_random(float piecemask)
+{
+	if ( !piecemask )
+		return 0;
+	
+	float i;
+	float f = 1;
+	
+	RandomSelection_Init();
+	
+	for ( i = 0; i < 9; i++ )
+	{
+		if ( piecemask & f )
+			RandomSelection_Add(world, f, string_null, 1, 1);
+		f <<= 1;
+	}
+	
+	dprint(sprintf("TTT AI: selected %x from %x\n",
+			RandomSelection_chosen_float, piecemask) );
+	return RandomSelection_chosen_float;
+}
+
+// Block/complete a 3 i na row
+float ttt_ai_block3 ( float piecemask, float piecemask_free )
+{
+	float r = 0;
+	
+	r |= ttt_ai_1of3(piecemask,TTT_AI_POSFLAG_A1,TTT_AI_POSFLAG_A2,TTT_AI_POSFLAG_A3);
+	r |= ttt_ai_1of3(piecemask,TTT_AI_POSFLAG_B1,TTT_AI_POSFLAG_B2,TTT_AI_POSFLAG_B3);
+	r |= ttt_ai_1of3(piecemask,TTT_AI_POSFLAG_C1,TTT_AI_POSFLAG_C2,TTT_AI_POSFLAG_C3);
+	r |= ttt_ai_1of3(piecemask,TTT_AI_POSFLAG_A1,TTT_AI_POSFLAG_B1,TTT_AI_POSFLAG_C1);
+	r |= ttt_ai_1of3(piecemask,TTT_AI_POSFLAG_A2,TTT_AI_POSFLAG_B2,TTT_AI_POSFLAG_C2);
+	r |= ttt_ai_1of3(piecemask,TTT_AI_POSFLAG_A3,TTT_AI_POSFLAG_B3,TTT_AI_POSFLAG_C3);
+	r |= ttt_ai_1of3(piecemask,TTT_AI_POSFLAG_A1,TTT_AI_POSFLAG_B2,TTT_AI_POSFLAG_C3);
+	r |= ttt_ai_1of3(piecemask,TTT_AI_POSFLAG_A3,TTT_AI_POSFLAG_B2,TTT_AI_POSFLAG_C1);
+	dprint(sprintf("TTT AI: possible 3 in a rows in %x: %x (%x)\n",piecemask,r, r&piecemask_free));
+	r &= piecemask_free;
+	return ttt_ai_random(r);
+}
+
+// Simple AI
+// 1) tries to win the game if possible
+// 2) tries to block the opponent if they have 2 in a row
+// 3) places a piece randomly
+string ttt_ai_choose_simple(float piecemask_self, float piecemask_opponent, float piecemask_free )
+{
+	float move = 0;
+	
+	dprint("TTT AI: checking winning move\n");
+	if (( move = ttt_ai_block3(piecemask_self,piecemask_free) ))
+		return ttt_ai_piece_flag2pos(move); // place winning move
+		
+	dprint("TTT AI: checking opponent's winning move\n");
+	if (( move = ttt_ai_block3(piecemask_opponent,piecemask_free) ))
+		return ttt_ai_piece_flag2pos(move); // block opponent
+		
+	dprint("TTT AI: random move\n");
+	return ttt_ai_piece_flag2pos(ttt_ai_random(piecemask_free));
+}
+
+// AI move (if it's AI's turn)
+void ttt_aimove(entity minigame)
+{
+	if ( minigame.minigame_flags == (TTT_TURN_PLACE|minigame.ttt_ai) )
+	{
+		entity aiplayer = world;
+		while ( ( aiplayer = findentity(aiplayer,owner,minigame) ) )
+			if ( aiplayer.classname == "minigame_player" && !aiplayer.minigame_playerslot )
+				break;
+		
+		/*
+		 * Build bit masks for the board pieces
+		 * .---.---.---.
+		 * | 4 | 32|256| 3
+		 * |---+---+---| 
+		 * | 2 | 16|128| 2
+		 * |---+---+---| 
+		 * | 1 | 8 | 64| 1
+		 * '---'---'---'
+		 *   A   B   C
+		 */
+		float piecemask_self = 0;
+		float piecemask_opponent = 0;
+		float piecemask_free = 0;
+		float pieceflag = 1;
+		string pos;
+		
+		float i,j;
+		for ( i = 0; i < 3; i++ )
+			for ( j = 0; j < 3; j++ )
+			{
+				pos = minigame_tile_buildname(i,j);
+				entity piece = ttt_find_piece(minigame,pos);
+				if ( piece )
+				{
+					if ( piece.team == aiplayer.team )
+						piecemask_self |= pieceflag;
+					else
+						piecemask_opponent |= pieceflag;
+				}
+				else
+					piecemask_free |= pieceflag;
+				pieceflag <<= 1;
+			}
+			
+		// TODO multiple AI difficulties
+		dprint(sprintf("TTT AI: self: %x opponent: %x free: %x\n",
+				piecemask_self, piecemask_opponent, piecemask_free));
+		pos = ttt_ai_choose_simple(piecemask_self, piecemask_opponent, piecemask_free);
+		dprint("TTT AI: chosen move: ",pos,"\n\n");
+		if ( !pos )
+			dprint("Tic Tac Toe AI has derped!\n");
+		else
+			ttt_move(minigame,aiplayer,pos);
+	}
+	minigame.message = ttt_turn_to_string(minigame.minigame_flags);
+}
+
+// Make the correct move
+void ttt_make_move(entity minigame)
+{
+	if ( minigame.minigame_flags == (TTT_TURN_PLACE|minigame_self.team) )
+	{
+		if ( minigame.ttt_ai  )
+		{
+			ttt_move(minigame, minigame_self, ttt_curr_pos );
+			ttt_aimove(minigame);
+		}
+		else
+			minigame_cmd("move ",ttt_curr_pos);
+	}
+}
+
+void ttt_set_curr_pos(string s)
+{
+	if ( ttt_curr_pos )
+		strunzone(ttt_curr_pos);
+	if ( s )
+		s = strzone(s);
+	ttt_curr_pos = s;
+}
+
+// Required function, handle client events
+float minigame_event_ttt(entity minigame, string event, ...)
+{
+	switch(event)
+	{
+		case "activate":
+		{
+			ttt_set_curr_pos("");
+			minigame.message = ttt_turn_to_string(minigame.minigame_flags);
+			return false;
+		}
+		case "key_pressed":
+		{
+			if((minigame.minigame_flags & TTT_TURN_TEAM) == minigame_self.team)
+			{
+				switch ( ...(0,float) )
+				{
+					case K_RIGHTARROW:
+					case K_KP_RIGHTARROW:
+						if ( ! ttt_curr_pos )
+							ttt_set_curr_pos("a3");
+						else
+							ttt_set_curr_pos(minigame_relative_tile(ttt_curr_pos,1,0,3,3));
+						return true;
+					case K_LEFTARROW:
+					case K_KP_LEFTARROW:
+						if ( ! ttt_curr_pos )
+							ttt_set_curr_pos("c3");
+						else
+							ttt_set_curr_pos(minigame_relative_tile(ttt_curr_pos,-1,0,3,3));
+						return true;
+					case K_UPARROW:
+					case K_KP_UPARROW:
+						if ( ! ttt_curr_pos )
+							ttt_set_curr_pos("a1");
+						else
+							ttt_set_curr_pos(minigame_relative_tile(ttt_curr_pos,0,1,3,3));
+						return true;
+					case K_DOWNARROW:
+					case K_KP_DOWNARROW:
+						if ( ! ttt_curr_pos )
+							ttt_set_curr_pos("a3");
+						else
+							ttt_set_curr_pos(minigame_relative_tile(ttt_curr_pos,0,-1,3,3));
+						return true;
+					case K_ENTER:
+					case K_KP_ENTER:
+					case K_SPACE:
+						ttt_make_move(minigame);
+						return true;
+				}
+			}
+
+			return false;
+		}
+		case "mouse_pressed":
+		{
+			if(...(0,float) == K_MOUSE1)
+			{
+				ttt_make_move(minigame);
+				return true;
+			}
+
+			return false;
+		}
+		case "mouse_moved":
+		{
+			vector mouse_pos = minigame_hud_normalize(mousepos,ttt_boardpos,ttt_boardsize);
+			if ( minigame.minigame_flags == (TTT_TURN_PLACE|minigame_self.team) )
+				ttt_set_curr_pos(minigame_tile_name(mouse_pos,3,3));
+			if ( ! ttt_valid_tile(ttt_curr_pos) )
+				ttt_set_curr_pos("");
+
+			return true;
+		}
+		case "network_receive":
+		{
+			entity sent = ...(0,entity);
+			float sf = ...(1,float);
+			if ( sent.classname == "minigame" )
+			{
+				if ( sf & MINIG_SF_UPDATE )
+				{
+					sent.message = ttt_turn_to_string(sent.minigame_flags);
+					if ( sent.minigame_flags & minigame_self.team )
+						minigame_prompt();
+				}
+				
+				if ( (sf & TTT_SF_SINGLEPLAYER) )
+				{
+					float ai = ReadByte();
+					float spawnai = ai && !sent.ttt_ai;
+					sent.ttt_ai = ai;
+					
+					if ( spawnai )
+					{
+						entity aiplayer = spawn();
+						aiplayer.classname = "minigame_player";
+						aiplayer.owner = minigame;
+						aiplayer.team = ai;
+						aiplayer.minigame_playerslot = 0;
+						aiplayer.minigame_autoclean = 1;
+						ttt_aimove(minigame);
+					}
+					
+				}
+			}
+			else if ( sent.classname == "minigame_player" && (sf & TTT_SF_PLAYERSCORE ) )
+			{
+				sent.minigame_flags = ReadByte();
+			}
+
+			return false;
+		}
+		case "menu_show":
+		{
+			HUD_MinigameMenu_CustomEntry(...(0,entity),_("Next Match"),"next");
+			HUD_MinigameMenu_CustomEntry(...(0,entity),_("Single Player"),"singleplayer");
+			return false;
+		}
+		case "menu_click":
+		{
+			if(...(0,string) == "next")
+			{
+				if ( minigame.ttt_ai )
+				{
+					ttt_next_match(minigame,minigame_self);
+					ttt_aimove(minigame);
+				}
+				else
+					minigame_cmd("next");
+			}
+			else if ( ...(0,string) == "singleplayer" && !minigame.ttt_ai )
+			{
+				if ( minigame_count_players(minigame) == 1 )
+					minigame_cmd("singleplayer");
+			}
+			return false;
+		}
+	}
+
+	return false;
+}
+
+#endif
\ No newline at end of file
diff --git a/qcsrc/common/minigames/minigames.qc b/qcsrc/common/minigames/minigames.qc
new file mode 100644
index 0000000000..46f0a9c574
--- /dev/null
+++ b/qcsrc/common/minigames/minigames.qc
@@ -0,0 +1,131 @@
+#include "minigames.qh"
+
+entity minigame_get_descriptor(string id)
+{
+	entity e;
+	for ( e = minigame_descriptors; e != world; e = e.list_next )
+		if ( e.netname == id )
+			return e;
+	return world;
+}
+
+// Get letter index of a tile name
+float minigame_tile_letter(string id)
+{
+	return str2chr(substring(id,0,1),0)-'a';
+}
+
+// Get number index of a tile name
+// Note: this is 0 based, useful for mathematical operations
+// Note: Since the tile notation starts from the bottom left, 
+//	you may want to do number_of_rows - what_this_function_returns or something
+float minigame_tile_number(string id)
+{
+	return stof(substring(id,1,-1)) -1 ;
+}
+
+// Get relative position of the center of a given tile
+vector minigame_tile_pos(string id, float rows, float columns)
+{
+	return eX*(minigame_tile_letter(id)+0.5)/columns + 
+	       eY - eY*(minigame_tile_number(id)+0.5)/rows;
+}
+
+// Get a tile name from indices
+string minigame_tile_buildname(float letter, float number)
+{
+	return strcat(chr2str('a'+letter),ftos(number+1));
+}
+
+// Get the id of a tile relative to the given one
+string minigame_relative_tile(string start_id, float dx, float dy, float rows, float columns)
+{
+	float letter = minigame_tile_letter(start_id);
+	float number = minigame_tile_number(start_id);
+	letter = (letter+dx) % columns;
+	number = (number+dy) % rows;
+	if ( letter < 0 )
+		letter = columns + letter;
+	if ( number < 0 )
+		number = rows + number;
+	return minigame_tile_buildname(letter, number);
+}
+
+// Get tile name from a relative position (matches the tile covering a square area)
+string minigame_tile_name(vector pos, float rows, float columns)
+{
+	if ( pos_x < 0 || pos_x > 1 || pos_y < 0 || pos_y > 1 )
+		return ""; // no tile
+		
+	float letter = floor(pos_x * columns);
+	float number = floor((1-pos_y) * rows);
+	return minigame_tile_buildname(letter, number);
+}
+
+// Get the next team number (note: team numbers are between 1 and n_teams, inclusive)
+float minigame_next_team(float curr_team, float n_teams)
+{
+	return curr_team % n_teams + 1;
+}
+
+// set send flags only when on server
+// (for example in game logic which can be used both in client and server
+void minigame_server_sendflags(entity ent, float mgflags)
+{
+	#ifdef SVQC
+		ent.SendFlags |= mgflags;
+	#endif
+}
+
+// Spawn linked entity on the server or local entity on the client
+// This entity will be removed automatically when the minigame ends
+entity msle_spawn(entity minigame_session, string class_name)
+{
+	entity e = spawn();
+	e.classname = class_name;
+	e.owner = minigame_session;
+	e.minigame_autoclean = 1;
+	#ifdef SVQC
+		e.customizeentityforclient = minigame_CheckSend;
+		Net_LinkEntity(e, false, 0, minigame_SendEntity);
+	#endif
+	return e;
+}
+
+const float msle_base_id = 2;
+float msle_id(string class_name)
+{
+	if ( class_name == "minigame" ) return 1;
+	if ( class_name == "minigame_player" ) return 2;
+	float i = msle_base_id;
+#define MSLE(Name, Fields) i++; if ( class_name == #Name ) return i;
+	MINIGAME_SIMPLELINKED_ENTITIES
+#undef MSLE
+	return 0;
+}
+
+string msle_classname(float id)
+{
+	if ( id == 1 ) return "minigame";
+	if ( id == 2 ) return "minigame_player";
+	float i = msle_base_id;
+#define MSLE(Name, Fields) i++; if ( id == i ) return #Name;
+	MINIGAME_SIMPLELINKED_ENTITIES
+#undef MSLE
+	return "";
+}
+
+float minigame_count_players(entity minigame)
+{
+	float pl_num = 0;
+	entity e;
+#ifdef SVQC
+	for(e = minigame.minigame_players; e; e = e.list_next)
+#elif defined(CSQC)
+	e = world;
+	while( (e = findentity(e,owner,minigame)) )
+		if ( e.classname == "minigame_player" )
+#endif
+		pl_num++;
+	return pl_num;
+}
\ No newline at end of file
diff --git a/qcsrc/common/minigames/minigames.qh b/qcsrc/common/minigames/minigames.qh
new file mode 100644
index 0000000000..3b8bbafb46
--- /dev/null
+++ b/qcsrc/common/minigames/minigames.qh
@@ -0,0 +1,124 @@
+#ifndef MINIGAMES_H
+#define MINIGAMES_H
+
+entity minigame_descriptors;
+
+// previous node in a doubly linked list
+.entity list_prev;
+// next node in a linked list
+.entity list_next;
+
+entity minigame_get_descriptor(string id);
+
+// Get letter index of a tile name
+float minigame_tile_letter(string id);
+
+// Get number index of a tile name
+// Note: this is 0 based, useful for mathematical operations
+// Note: Since the tile notation starts from the bottom left, 
+//	you may want to do number_of_rows - what_this_function_returns or something
+float minigame_tile_number(string id);
+
+// Get relative position of the center of a given tile
+vector minigame_tile_pos(string id, float rows, float columns);
+
+// Get a tile name from indices
+string minigame_tile_buildname(float letter, float number);
+
+// Get the id of a tile relative to the given one
+string minigame_relative_tile(string start_id, float dx, float dy, float rows, float columns);
+
+// Get tile name from a relative position (matches the tile covering a square area)
+string minigame_tile_name(vector pos, float rows, float columns);
+
+// Get the next team number (note: team numbers are between 1 and n_teams, inclusive)
+float minigame_next_team(float curr_team, float n_teams);
+
+// set send flags only when on server
+// (for example in game logic which can be used both in client and server
+void minigame_server_sendflags(entity ent, float mgflags);
+
+// count the number of players in a minigame session
+float minigame_count_players(entity minigame);
+
+/// For minigame sessions: minigame descriptor object
+.entity descriptor;
+
+/// For minigame sessions/descriptors: execute the given event
+/// Client events:
+/// 	mouse_moved(vector mouse_pos)
+///			return 1 to handle input, 0 to discard
+/// 	mouse_pressed/released(float K_Keycode)
+///			return 1 to handle input, 0 to discard
+/// 		note: see dpdefs/keycodes.qc for values
+/// 	key_pressed/released(float K_Keycode)
+/// 		return 1 to handle input, 0 to discard
+/// 		note: see dpdefs/keycodes.qc for values
+/// 	activate()
+/// 		executed when the minigame is activated for the current client
+/// 	deactivate()
+/// 		executed when the minigame is deactivated for the current client
+/// 	network_receive(entity received,float flags)
+/// 		executed each time a networked entity is received
+/// 		note: when this is called self == ...(0,entity)
+/// 		You can use the MINIG_SF_ constants to check the send flags
+/// 		IMPORTANT: always read in client everything you send from the server!
+/// 	menu_show(entity parent_menu_item)
+/// 		executed when the Current Game menu is shown, used to add custom entries
+/// 		Call HUD_MinigameMenu_CustomEntry to do so (pass ...(0,entity) as first argument)
+/// 	menu_click(string arg)
+/// 		executed when a custom menu entry is clicked
+/// Server events:
+/// 	start()
+/// 		executed when the minigame session is starting
+/// 	end()
+/// 		executed when the minigame session is shutting down
+/// 	join(entity player)
+/// 		executed when a player wants to join the session
+/// 		return the player team number to accept the new player, 0 to discard
+/// 	part(entity player)
+/// 		executed when a player is going to leave the session
+/// 	network_send(entity sent,float flags)
+/// 		executed each time a networked entity is sent
+/// 		note: when this is called self == ...(0,entity)
+/// 		You can use the MINIG_SF_ constants to check the send flags
+/// 		IMPORTANT: always read in client everything you send from the server!
+/// 	cmd(entity minigame_player, float argc, string command)
+/// 		self = client entity triggering this
+/// 		argv(n) = console token 
+/// 		argc: number of console tokens
+/// 		command: full command string
+/// 		triggered when a player does "cmd minigame ..." with some unrecognized command
+/// 		return 1 if the minigame has handled the command
+/// 	impulse(entity minigame_player,float impulse)
+/// 		self = client entity triggering this
+/// 		triggered when a player does "impulse ..."
+/// 		return 1 if the minigame has handled the impulse
+.float(entity,string,...)   minigame_event;
+
+// For run-time gameplay entities: Whether to be removed when the game is deactivated
+.float minigame_autoclean;
+
+// For run-time gameplay entities: some place to store flags safely
+.float minigame_flags;
+
+// Send flags, set to .SendFlags on networked entities to send entity information
+// Flag values for customized events must be powers of 2 in the range
+// [MINIG_SF_CUSTOM, MINIG_SF_MAX] (inclusive)
+const float MINIG_SF_CREATE  = 0x01; // Create a new object
+const float MINIG_SF_UPDATE  = 0x02; // miscellaneous entity update
+const float MINIG_SF_CUSTOM  = 0x10; // a customized networked event
+const float MINIG_SF_MAX     = 0x80; // maximum flag value sent over the network
+const float MINIG_SF_ALL     = 0xff; // use to resend everything
+
+
+// Spawn linked entity on the server or local entity on the client
+// This entity will be removed automatically when the minigame ends
+entity msle_spawn(entity minigame_session, string class_name);
+
+#include "minigame/all.qh"
+
+float msle_id(string class_name);
+string msle_classname(float id);
+
+#endif
diff --git a/qcsrc/common/minigames/sv_minigames.qc b/qcsrc/common/minigames/sv_minigames.qc
new file mode 100644
index 0000000000..1953e9674e
--- /dev/null
+++ b/qcsrc/common/minigames/sv_minigames.qc
@@ -0,0 +1,430 @@
+#include "minigames.qh"
+
+void player_clear_minigame(entity player)
+{
+	player.active_minigame = world;
+	if ( IS_PLAYER(player) )
+		player.movetype = MOVETYPE_WALK;
+	else
+		player.movetype = MOVETYPE_FLY_WORLDONLY;
+	player.team_forced = 0;
+}
+
+void minigame_rmplayer(entity minigame_session, entity player)
+{
+	entity e;
+	entity p = minigame_session.minigame_players;
+	
+	if ( p.minigame_players == player )
+	{
+		if ( p.list_next == world )
+		{
+			end_minigame(minigame_session);
+			return;
+		}
+		minigame_session.minigame_event(minigame_session,"part",player);
+		GameLogEcho(strcat(":minigame:part:",minigame_session.netname,":",
+			ftos(num_for_edict(player)),":",player.netname));
+		minigame_session.minigame_players = p.list_next;
+		remove ( p );
+		player_clear_minigame(player);
+	}
+	else
+	{
+		for ( e = p.list_next; e != world; e = e.list_next )
+		{
+			if ( e.minigame_players == player )
+			{
+				minigame_session.minigame_event(minigame_session,"part",player);
+				GameLogEcho(strcat(":minigame:part:",minigame_session.netname,":",
+					ftos(num_for_edict(player)),":",player.netname));
+				p.list_next = e.list_next;
+				remove(e);
+				player_clear_minigame(player);
+				return;
+			}
+			p = e;
+		}
+	}
+}
+
+
+#define FIELD(Flags, Type,Name) if ( sf & (Flags) ) Write##Type(MSG_ENTITY, self.Name);
+#define WriteVector(to,Name) WriteCoord(to,Name##_x); WriteCoord(to,Name##_y); WriteCoord(to,Name##_z)
+#define WriteVector2D(to,Name) WriteCoord(to,Name##_x); WriteCoord(to,Name##_y)
+#define WriteFloat WriteCoord
+#define MSLE(Name,Fields) \
+	else if ( self.classname == #Name ) { \
+		if ( sf & MINIG_SF_CREATE ) WriteString(MSG_ENTITY,self.owner.netname); \
+		Fields }
+
+// Send an entity to a client
+// only use on minigame entities or entities with a minigame owner
+float minigame_SendEntity(entity to, float sf)
+{
+	WriteByte(MSG_ENTITY, ENT_CLIENT_MINIGAME);
+	WriteByte(MSG_ENTITY, sf);
+	
+	if ( sf & MINIG_SF_CREATE )
+	{
+		WriteShort(MSG_ENTITY,msle_id(self.classname));
+		WriteString(MSG_ENTITY,self.netname);
+	}
+	
+	entity minigame_ent = self.owner;
+	
+	if ( self.classname == "minigame" )
+	{
+		minigame_ent = self;
+		
+		if ( sf & MINIG_SF_CREATE )
+			WriteString(MSG_ENTITY,self.descriptor.netname);
+		
+		if ( sf & MINIG_SF_UPDATE )
+			WriteLong(MSG_ENTITY,self.minigame_flags);
+	}
+	else if ( self.classname == "minigame_player" )
+	{
+		if ( sf & MINIG_SF_CREATE )
+		{
+			WriteString(MSG_ENTITY,self.owner.netname);
+			WriteLong(MSG_ENTITY,num_for_edict(self.minigame_players));
+		}
+		if ( sf & MINIG_SF_UPDATE )
+			WriteByte(MSG_ENTITY,self.team);
+	}
+	MINIGAME_SIMPLELINKED_ENTITIES
+	
+	minigame_ent.minigame_event(minigame_ent,"network_send",self,sf);
+	
+	return 1;
+	
+}
+#undef FIELD
+#undef MSLE
+#undef WriteFloat
+
+// Force resend all minigame entities
+void minigame_resend(entity minigame)
+{
+	minigame.SendFlags = MINIG_SF_ALL;
+	entity e = world;
+	while (( e = findentity(e,owner,minigame) ))
+	{
+		e.SendFlags = MINIG_SF_ALL;
+	}
+}
+
+float minigame_CheckSend()
+{
+	entity e;
+	for ( e = self.owner.minigame_players; e != world; e = e.list_next )
+		if ( e.minigame_players == other )
+			return 1;
+	return 0;
+}
+
+float minigame_addplayer(entity minigame_session, entity player)
+{
+	if ( player.active_minigame )
+	{
+		if ( player.active_minigame == minigame_session )
+			return 0;
+		minigame_rmplayer(player.active_minigame,player);
+	}
+	
+	float mgteam = minigame_session.minigame_event(minigame_session,"join",player);
+	
+	if ( mgteam )
+	{
+		entity player_pointer = spawn();
+		player_pointer.classname = "minigame_player";
+		player_pointer.owner = minigame_session;
+		player_pointer.minigame_players = player;
+		player_pointer.team = mgteam;
+		player_pointer.list_next = minigame_session.minigame_players;
+		minigame_session.minigame_players = player_pointer;
+		player.active_minigame = minigame_session;
+		player_pointer.customizeentityforclient = minigame_CheckSend;
+		Net_LinkEntity(player_pointer, false, 0, minigame_SendEntity);
+
+		if ( !IS_OBSERVER(player) && autocvar_sv_minigames_observer )
+		{
+			entity e = self;
+			self = player;
+			PutObserverInServer();
+			self = e;
+		}
+		if ( autocvar_sv_minigames_observer == 2 )
+			player.team_forced = -1;
+		
+		minigame_resend(minigame_session);
+	}
+	GameLogEcho(strcat(":minigame:join",(mgteam?"":"fail"),":",minigame_session.netname,":",
+		ftos(num_for_edict(player)),":",player.netname));
+	
+	return mgteam;
+}
+
+entity start_minigame(entity player, string minigame )
+{
+	if ( !autocvar_sv_minigames || !IS_REAL_CLIENT(player) )
+		return world;
+	
+	entity e = minigame_get_descriptor(minigame);
+	if ( e ) 
+	{
+		entity minig = spawn();
+		minig.classname = "minigame";
+		minig.netname = strzone(strcat(e.netname,"_",ftos(num_for_edict(minig))));
+		minig.descriptor = e;
+		minig.minigame_event = e.minigame_event;
+		minig.minigame_event(minig,"start");
+		GameLogEcho(strcat(":minigame:start:",minig.netname));
+		if ( ! minigame_addplayer(minig,player) )
+		{
+			dprint("Minigame ",minig.netname," rejected the first player join!\n");
+			end_minigame(minig);
+			return world;
+		}
+		Net_LinkEntity(minig, false, 0, minigame_SendEntity);
+		
+		if ( !minigame_sessions )
+			minigame_sessions = minig;
+		else
+		{
+			minigame_sessions.owner = minig;
+			minig.list_next = minigame_sessions;
+			minigame_sessions = minig;
+		}
+		return minig;
+	}
+		
+	return world;
+}
+
+entity join_minigame(entity player, string game_id )
+{
+	if ( !autocvar_sv_minigames || !IS_REAL_CLIENT(player) )
+		return world;
+	
+	entity minig;
+	for ( minig = minigame_sessions; minig != world; minig = minig.list_next )
+	{
+		if ( minig.netname == game_id )
+		if ( minigame_addplayer(minig,player) )
+			return minig;
+	}
+	
+	return world;
+}
+
+void part_minigame(entity player )
+{
+	entity minig = player.active_minigame;
+	
+	if ( minig && minig.classname == "minigame" )
+		minigame_rmplayer(minig,player);
+}
+
+void end_minigame(entity minigame_session)
+{
+	if ( minigame_session.owner )
+		minigame_session.owner.list_next = minigame_session.list_next;
+	else
+		minigame_sessions = minigame_session.list_next;
+	
+	minigame_session.minigame_event(minigame_session,"end");
+	GameLogEcho(strcat(":minigame:end:",minigame_session.netname));
+	
+	
+	entity e = world;
+	while( (e = findentity(e, owner, minigame_session)) )
+		if ( e.minigame_autoclean )
+		{
+			dprint("SV Auto-cleaned: ",ftos(num_for_edict(e)), " (",e.classname,")\n");
+			remove(e);
+		}
+	
+	entity p;
+	for ( e = minigame_session.minigame_players; e != world; e = p )
+	{
+		p = e.list_next;
+		player_clear_minigame(e.minigame_players);
+		remove(e);
+	}
+	
+	strunzone(minigame_session.netname);
+	remove(minigame_session);
+}
+
+void end_minigames()
+{
+	while ( minigame_sessions )
+	{
+		end_minigame(minigame_sessions);
+	}
+}
+
+void initialize_minigames()
+{
+	entity last_minig = world;
+	entity minig;
+	#define MINIGAME(name,nicename) \
+		minig = spawn(); \
+		minig.classname = "minigame_descriptor"; \
+		minig.netname = #name; \
+		minig.message = nicename; \
+		minig.minigame_event = minigame_event_##name; \
+		if ( !last_minig ) minigame_descriptors = minig; \
+		else last_minig.list_next = minig; \
+		last_minig = minig;
+		
+	REGISTERED_MINIGAMES
+	
+	#undef MINIGAME
+}
+
+string invite_minigame(entity inviter, entity player)
+{
+	if ( !inviter || !inviter.active_minigame )
+		return "Invalid minigame";
+	if ( !VerifyClientEntity(player, true, false) )
+		return "Invalid player";
+	if ( inviter == player )
+		return "You can't invite yourself";
+	if ( player.active_minigame == inviter.active_minigame )
+		return strcat(player.netname," is already playing");
+	
+	Send_Notification(NOTIF_ONE, player, MSG_INFO, INFO_MINIGAME_INVITE, 
+		inviter.active_minigame.netname, inviter.netname );
+	
+	GameLogEcho(strcat(":minigame:invite:",inviter.active_minigame.netname,":",
+		ftos(num_for_edict(player)),":",player.netname));
+	
+	return "";
+}
+
+entity minigame_find_player(entity client)
+{
+	if ( ! client.active_minigame )
+		return world;
+	entity e;
+	for ( e = client.active_minigame.minigame_players; e; e = e.list_next )
+		if ( e.minigame_players == client )
+			return e;
+	return world;
+}
+
+float MinigameImpulse(float imp)
+{
+	entity e = minigame_find_player(self);
+	if ( imp && self.active_minigame && e )
+	{
+		return self.active_minigame.minigame_event(self.active_minigame,"impulse",e,imp);
+	}
+	return 0;
+}
+
+
+
+void ClientCommand_minigame(float request, float argc, string command)
+{
+	if ( !autocvar_sv_minigames )
+	{
+		sprint(self,"Minigames are not enabled!\n");
+		return;
+	}
+	
+	if (request == CMD_REQUEST_COMMAND )
+	{
+		string minig_cmd = argv(1);
+		if ( minig_cmd == "create" && argc > 2 )
+		{
+			entity minig = start_minigame(self, argv(2));
+			if ( minig )
+				sprint(self,"Created minigame session: ",minig.netname,"\n");
+			else
+				sprint(self,"Cannot start minigame session!\n");
+			return;
+		}
+		else if ( minig_cmd == "join" && argc > 2 )
+		{
+			entity minig = join_minigame(self, argv(2));
+			if ( minig )
+				sprint(self,"Joined: ",minig.netname,"\n");
+			else
+			{
+				Send_Notification(NOTIF_ONE, self, MSG_CENTER, CENTER_JOIN_PREVENT_MINIGAME);
+				sprint(self,"Cannot join given minigame session!\n");
+			}
+			return;
+		}
+		else if ( minig_cmd == "list" )
+		{
+			entity e;
+			for ( e = minigame_descriptors; e != world; e = e.list_next )
+				sprint(self,e.netname," (",e.message,") ","\n");
+			return;
+		}
+		else if ( minig_cmd == "list-sessions" )
+		{
+			entity e;
+			for ( e = minigame_sessions; e != world; e = e.list_next )
+				sprint(self,e.netname,"\n");
+			return;
+		}
+		else if ( minig_cmd == "end" || minig_cmd == "part" )
+		{
+			if ( self.active_minigame )
+			{
+				part_minigame(self);
+				sprint(self,"Left minigame session\n");
+			}
+			else
+				sprint(self,"You aren't playing any minigame...\n");
+			return;
+		}
+		else if ( minig_cmd == "invite" && argc > 2 )
+		{
+			if ( self.active_minigame )
+			{
+				entity client = GetIndexedEntity(argc, 2);
+				string error = invite_minigame(self,client);
+				if ( error == "" )
+				{
+					sprint(self,"You have invited ",client.netname,
+						" to join your game of ", self.active_minigame.descriptor.message, "\n");
+				}
+				else
+					sprint(self,"Could not invite: ", error, ".\n");
+			}
+			else
+				sprint(self,"You aren't playing any minigame...\n");
+			return;
+		}
+		else if ( self.active_minigame )
+		{
+			entity e = minigame_find_player(self);
+			string subcommand = substring(command,argv_end_index(0),-1);
+			float arg_c = tokenize_console(subcommand);
+			if ( self.active_minigame.minigame_event(self.active_minigame,"cmd",e,arg_c,subcommand) )
+				return;
+				
+		}
+		else sprint(self,strcat("Wrong command:^1 ",command,"\n"));
+	}
+	
+	sprint(self, "\nUsage:^3 cmd minigame create <minigame>\n");
+	sprint(self, "  Start a new minigame session\n");
+	sprint(self, "Usage:^3 cmd minigame join <session>\n");
+	sprint(self, "  Join an exising minigame session\n");
+	sprint(self, "Usage:^3 cmd minigame list\n");
+	sprint(self, "  List available minigames\n");
+	sprint(self, "Usage:^3 cmd minigame list-sessions\n");
+	sprint(self, "  List available minigames sessions\n");
+	sprint(self, "Usage:^3 cmd minigame part|end\n");
+	sprint(self, "  Leave the current minigame\n");
+	sprint(self, "Usage:^3 cmd minigame invite <player>\n");
+	sprint(self, "  Invite the given player to join you in a minigame\n");
+}
\ No newline at end of file
diff --git a/qcsrc/common/minigames/sv_minigames.qh b/qcsrc/common/minigames/sv_minigames.qh
new file mode 100644
index 0000000000..c9591bb131
--- /dev/null
+++ b/qcsrc/common/minigames/sv_minigames.qh
@@ -0,0 +1,54 @@
+#ifndef SV_MINIGAMES_H
+#define SV_MINIGAMES_H
+
+/// Initialize the minigame system
+void initialize_minigames();
+
+/// Create a new minigame session
+/// \return minigame session entity
+entity start_minigame(entity player, string minigame );
+
+/// Join an existing minigame session
+/// \return minigame session entity
+entity join_minigame(entity player, string game_id );
+
+/// Invite a player to join in a minigame
+/// \return Error string
+string invite_minigame(entity inviter, entity player);
+
+// Part minigame session
+void part_minigame(entity player);
+
+// Ends a minigame session
+void end_minigame(entity minigame_session);
+
+// Ends all minigame sessions
+void end_minigames();
+
+// Only sends entities to players who joined the minigame
+// Use on customizeentityforclient for gameplay entities
+float minigame_CheckSend();
+
+// Check for minigame impulses
+float MinigameImpulse(float imp);
+
+// Parse a client command ( cmd minigame ... )
+void ClientCommand_minigame(float request, float argc, string command);
+
+// Find the minigame_player entity for the given client entity
+entity minigame_find_player(entity client);
+
+/// For players: Minigame being played
+.entity active_minigame;
+
+/// For minigame sessions: list of players
+/// For minigame_player: client entity
+.entity minigame_players;
+
+entity minigame_sessions;
+
+float minigame_SendEntity(entity to, float sf);
+
+var void remove(entity e);
+
+#endif
diff --git a/qcsrc/common/notifications.qc b/qcsrc/common/notifications.qc
index b48daec7bc..4c2c30ebb8 100644
--- a/qcsrc/common/notifications.qc
+++ b/qcsrc/common/notifications.qc
@@ -1562,6 +1562,14 @@ void Local_Notification(int net_type, int net_name, ...count)
 			#ifdef CSQC
 			if(notif.nent_icon != "")
 			{
+				if ( notif.nent_iconargs != "" )
+				{
+					notif.nent_icon = Local_Notification_sprintf(
+						notif.nent_icon,notif.nent_iconargs,
+						s1, s2, s3, s4, f1, f2, f3, f4);
+					// remove the newline added by Local_Notification_sprintf
+					notif.nent_icon = strzone(substring(notif.nent_icon,0,strlen(notif.nent_icon)-1));
+				}
 				Local_Notification_HUD_Notify_Push(
 					notif.nent_icon,
 					notif.nent_hudargs,
diff --git a/qcsrc/common/notifications.qh b/qcsrc/common/notifications.qh
index 1cb1adf51f..0a81a2c653 100644
--- a/qcsrc/common/notifications.qh
+++ b/qcsrc/common/notifications.qh
@@ -499,6 +499,7 @@ void Send_Notification_WOCOVA(
     MSG_INFO_NOTIF(1, INFO_RACE_NEW_IMPROVED,              1, 3, "s1 race_col f1ord race_col f2race_time race_diff", "s1 f2race_time",        "race_newtime",          _("^BG%s^BG improved their %s%s^BG place record with %s%s %s"), "") \
     MSG_INFO_NOTIF(1, INFO_RACE_NEW_MISSING_UID,           1, 1, "s1 f1race_time", "s1 f1race_time",                                          "race_newfail",          _("^BG%s^BG scored a new record with ^F2%s^BG, but unfortunately lacks a UID and will be lost."), "") \
     MSG_INFO_NOTIF(1, INFO_RACE_NEW_SET,                   1, 2, "s1 race_col f1ord race_col f2race_time", "s1 f2race_time",                  "race_newrecordserver",  _("^BG%s^BG set the %s%s^BG place record with %s%s"), "") \
+    MULTIICON_INFO(1, INFO_MINIGAME_INVITE,                2, 0, "s2 minigame1_name s1","s2",              "minigame1_d",                    "minigames/%s/icon_notif",_("^F4You have been invited by ^BG%s^F4 to join their game of ^F2%s^F4 (^F1%s^F4)"), "") \
     MULTITEAM_INFO(1, INFO_SCORES_, 4,                     0, 0, "", "",                            "",                     _("^TC^TT ^BGteam scores!"), "") \
     MSG_INFO_NOTIF(1, INFO_SPECTATE_WARNING,               0, 1, "f1secs", "",                      "",                     _("^F2You have to become a player within the next %s, otherwise you will be kicked, because spectating isn't allowed at this time!"), "") \
     MSG_INFO_NOTIF(1, INFO_SUPERWEAPON_PICKUP,             1, 0, "s1", "s1",                        "superweapons",         _("^BG%s^K1 picked up a Superweapon"), "") \
@@ -734,7 +735,8 @@ void Send_Notification_WOCOVA(
     MSG_CENTER_NOTIF(1, CENTER_TEAMCHANGE_SUICIDE,          0, 1, "",              CPID_TEAMCHANGE,       "1 f1", _("^K1Suicide in ^COUNT"), "") \
     MSG_CENTER_NOTIF(1, CENTER_TIMEOUT_BEGINNING,           0, 1, "",              CPID_TIMEOUT,          "1 f1", _("^F4Timeout begins in ^COUNT"), "") \
     MSG_CENTER_NOTIF(1, CENTER_TIMEOUT_ENDING,              0, 1, "",              CPID_TIMEOUT,          "1 f1", _("^F4Timeout ends in ^COUNT"), "") \
-    MSG_CENTER_NOTIF(1, CENTER_WEAPON_MINELAYER_LIMIT,      0, 1, "f1",            NO_CPID,               "0 0",  _("^BGYou cannot place more than ^F2%s^BG mines at a time"), "")
+    MSG_CENTER_NOTIF(1, CENTER_WEAPON_MINELAYER_LIMIT,      0, 1, "f1",            NO_CPID,               "0 0",  _("^BGYou cannot place more than ^F2%s^BG mines at a time"), "") \
+    MSG_CENTER_NOTIF(1, CENTER_JOIN_PREVENT_MINIGAME,       0, 0, "",              NO_CPID,               "0 0",  _("^K1Cannot join given minigame session!"), "" )
 
 #define MULTITEAM_MULTI2(default,prefix,anncepre,infopre,centerpre) \
     MSG_MULTI_NOTIF(default, prefix##RED, anncepre##RED, infopre##RED, centerpre##RED) \
@@ -1017,6 +1019,8 @@ float autocvar_notification_show_sprees_center_specialonly = true;
     item_centime: amount of time to display weapon message in centerprint
     item_buffname: return full name of a buff from buffid
     death_team: show the full name of the team a player is switching from
+    minigame1_name: return human readable name of a minigame from its id(s1)
+    minigame1_d: return descriptor name of a minigame from its id(s1)
 */
 
 const float NOTIF_MAX_ARGS = 7;
@@ -1072,7 +1076,9 @@ const float ARG_DC = 6; // unique result to durcnt/centerprint
     ARG_CASE(ARG_CS_SV,     "item_wepammo",  (s1 != "" ? sprintf(_(" with %s"), s1) : "")) \
     ARG_CASE(ARG_DC,        "item_centime",  ftos(autocvar_notification_item_centerprinttime)) \
     ARG_CASE(ARG_SV,        "death_team",    Team_ColoredFullName(f1)) \
-    ARG_CASE(ARG_CS,        "death_team",    Team_ColoredFullName(f1 - 1))
+    ARG_CASE(ARG_CS,        "death_team",    Team_ColoredFullName(f1 - 1)) \
+    ARG_CASE(ARG_CS_SV_HA,  "minigame1_name",find(world,netname,s1).descriptor.message) \
+    ARG_CASE(ARG_CS_SV_HA,  "minigame1_d",   find(world,netname,s1).descriptor.netname)
 
 #define NOTIF_HIT_MAX(count,funcname) do { \
     if(sel_num == count) { backtrace(sprintf("%s: Hit maximum arguments!\n", funcname)); break; } \
@@ -1464,6 +1470,50 @@ float notif_global_error;
     } \
     ACCUMULATE_FUNCTION(RegisterNotifications, RegisterNotification_##name);
 
+.string nent_iconargs;
+#define MULTIICON_INFO(default,name,strnum,flnum,args,hudargs,iconargs,icon,normal,gentle) \
+    NOTIF_ADD_AUTOCVAR(name, default) \
+    float name; \
+    void RegisterNotification_##name() \
+    { \
+        SET_FIELD_COUNT(name, NOTIF_FIRST, NOTIF_INFO_COUNT) \
+        CHECK_MAX_COUNT(name, NOTIF_INFO_MAX, NOTIF_INFO_COUNT, "MSG_INFO") \
+        Create_Notification_Entity( \
+            /* COMMON ======================== */ \
+            default,            /* var_default */ \
+            ACVNN(name),        /* var_cvar    */ \
+            MSG_INFO,           /* typeid      */ \
+            name,               /* nameid      */ \
+            strtoupper(#name),  /* namestring  */ \
+            strnum,             /* strnum      */ \
+            flnum,              /* flnum       */ \
+            /* ANNCE =========== */ \
+            NO_MSG,  /* channel  */ \
+            "",      /* snd      */ \
+            NO_MSG,  /* vol      */ \
+            NO_MSG,  /* position */ \
+            /* INFO & CENTER === */ \
+            args,     /* args    */ \
+            hudargs,  /* hudargs */ \
+            icon,     /* icon    */ \
+            NO_MSG,   /* cpid    */ \
+            "",       /* durcnt  */ \
+            normal,   /* normal  */ \
+            gentle,   /* gentle  */ \
+            /* MULTI ============= */ \
+            NO_MSG,  /* anncename  */ \
+            NO_MSG,  /* infoname   */ \
+            NO_MSG,  /* centername */ \
+            /* CHOICE ============== */ \
+            NO_MSG,   /* challow_def */ \
+            NO_MSG,   /* challow_var */ \
+            NO_MSG,   /* chtype      */ \
+            NO_MSG,   /* optiona     */ \
+            NO_MSG);  /* optionb     */ \
+        msg_info_notifs[name - 1].nent_iconargs = iconargs; \
+    } \
+    ACCUMULATE_FUNCTION(RegisterNotifications, RegisterNotification_##name);
+
 #define MSG_CENTER_NOTIF(default,name,strnum,flnum,args,cpid,durcnt,normal,gentle) \
     NOTIF_ADD_AUTOCVAR(name, default) \
     float name; \
diff --git a/qcsrc/dpdefs/csprogsdefs.qh b/qcsrc/dpdefs/csprogsdefs.qh
index 6f820c729f..79b3e9b354 100644
--- a/qcsrc/dpdefs/csprogsdefs.qh
+++ b/qcsrc/dpdefs/csprogsdefs.qh
@@ -421,6 +421,7 @@ float( float b, ... ) max = #95;
 float(float minimum, float val, float maximum) bound = #96;
 float(float f, float f) pow = #97;
 entity(entity start, .float fld, float match) findfloat = #98;
+entity(entity start, .entity fld, entity match) findentity = #98;
 float(string s) checkextension = #99;
 // FrikaC and Telejano range #100-#199
 
diff --git a/qcsrc/server/autocvars.qh b/qcsrc/server/autocvars.qh
index 419956f861..dc6a5f581a 100644
--- a/qcsrc/server/autocvars.qh
+++ b/qcsrc/server/autocvars.qh
@@ -881,4 +881,6 @@ float autocvar_g_buffs_vampire_damage_steal;
 float autocvar_g_buffs_invisible_alpha;
 float autocvar_g_buffs_flight_gravity;
 float autocvar_g_buffs_jump_height;
+bool autocvar_sv_minigames;
+bool autocvar_sv_minigames_observer;
 #endif
diff --git a/qcsrc/server/cl_client.qc b/qcsrc/server/cl_client.qc
index 9727cb9479..e2d156195b 100644
--- a/qcsrc/server/cl_client.qc
+++ b/qcsrc/server/cl_client.qc
@@ -19,6 +19,8 @@
 
 #include "../common/net_notice.qh"
 
+#include "../common/minigames/sv_minigames.qh"
+
 #include "../common/monsters/sv_monsters.qh"
 
 #include "../warpzonelib/server.qh"
@@ -1273,6 +1275,9 @@ void ClientDisconnect (void)
 
 	PlayerStats_GameReport_FinalizePlayer(self);
 
+	if ( self.active_minigame )
+		part_minigame(self);
+
 	if(IS_PLAYER(self)) { pointparticles(particleeffectnum("spawn_event_neutral"), self.origin, '0 0 0', 1); }
 
 	CheatShutdownClient();
@@ -1340,6 +1345,20 @@ void ClientDisconnect (void)
 }
 
 .float BUTTON_CHAT;
+float ChatBubbleCustomize()
+{
+	entity e = WaypointSprite_getviewentity(other), own = self.owner;
+
+	if(!own.deadflag && IS_PLAYER(own))
+	{
+		if(own.BUTTON_CHAT) { self.skin = 0; return true; }
+		if(own.active_minigame) { self.skin = 1; return true; }
+		if(SAME_TEAM(own, e) && e != own) { self.skin = 2; return true; }
+	}
+
+	return false;
+}
+
 void ChatBubbleThink()
 {
 	self.nextthink = time;
@@ -1350,10 +1369,6 @@ void ChatBubbleThink()
 		remove(self);
 		return;
 	}
-	if (self.owner.BUTTON_CHAT && !self.owner.deadflag)
-		self.model = self.mdl;
-	else
-		self.model = "";
 }
 
 void UpdateChatBubble()
@@ -1366,6 +1381,8 @@ void UpdateChatBubble()
 		self.chatbubbleentity = spawn();
 		self.chatbubbleentity.owner = self;
 		self.chatbubbleentity.exteriormodeltoclient = self;
+		self.chatbubbleentity.alpha = 1;
+		self.chatbubbleentity.customizeentityforclient = ChatBubbleCustomize;
 		self.chatbubbleentity.think = ChatBubbleThink;
 		self.chatbubbleentity.nextthink = time;
 		setmodel(self.chatbubbleentity, "models/misc/chatbubble.spr"); // precision set below
@@ -1373,7 +1390,7 @@ void UpdateChatBubble()
 		setorigin(self.chatbubbleentity, '0 0 15' + self.maxs.z * '0 0 1');
 		setattachment(self.chatbubbleentity, self, "");  // sticks to moving player better, also conserves bandwidth
 		self.chatbubbleentity.mdl = self.chatbubbleentity.model;
-		self.chatbubbleentity.model = "";
+		//self.chatbubbleentity.model = "";
 		self.chatbubbleentity.effects = EF_LOWPRECISION;
 	}
 }
@@ -2095,6 +2112,11 @@ void PrintWelcomeMessage()
 
 void ObserverThink()
 {
+	if ( self.impulse )
+	{
+		MinigameImpulse(self.impulse);
+		self.impulse = 0;
+	}
 	float prefered_movetype;
 	if (self.flags & FL_JUMPRELEASED) {
 		if (self.BUTTON_JUMP && !self.version_mismatch) {
@@ -2125,6 +2147,11 @@ void ObserverThink()
 
 void SpectatorThink()
 {
+	if ( self.impulse )
+	{
+		MinigameImpulse(self.impulse);
+		self.impulse = 0;
+	}
 	if (self.flags & FL_JUMPRELEASED) {
 		if (self.BUTTON_JUMP && !self.version_mismatch) {
 			self.flags &= ~FL_JUMPRELEASED;
diff --git a/qcsrc/server/cl_impulse.qc b/qcsrc/server/cl_impulse.qc
index 79de1d275a..350fcf6f4e 100644
--- a/qcsrc/server/cl_impulse.qc
+++ b/qcsrc/server/cl_impulse.qc
@@ -4,6 +4,8 @@
 
 #include "weapons/throwing.qh"
 
+#include "../common/minigames/sv_minigames.qh"
+
 #include "../common/weapons/weapons.qh"
 
 /*
@@ -54,6 +56,10 @@ void ImpulseCommands (void)
 		return;
 	self.impulse = 0;
 
+	if ( self.active_minigame )
+	if ( MinigameImpulse(imp) )
+		return;
+
 	// allow only weapon change impulses when not in round time
 	if(round_handler_IsActive() && !round_handler_IsRoundStarted())
 	if(imp == 17 || (imp >= 20 && imp < 200) || imp > 253)
diff --git a/qcsrc/server/cl_player.qc b/qcsrc/server/cl_player.qc
index 49d1c1b469..2afadfaac6 100644
--- a/qcsrc/server/cl_player.qc
+++ b/qcsrc/server/cl_player.qc
@@ -3,6 +3,8 @@
 #include "g_violence.qh"
 #include "miscfunctions.qh"
 
+#include "../common/minigames/sv_minigames.qh"
+
 #include "weapons/weaponstats.qh"
 
 void CopyBody_Think(void)
@@ -860,6 +862,15 @@ float Say(entity source, float teamsay, entity privatesay, string msgin, float f
 			if(cmsgstr != "")
 				centerprint(privatesay, cmsgstr);
 		}
+		else if ( teamsay && source.active_minigame )
+		{
+			sprint(source, sourcemsgstr);
+			dedicated_print(msgstr); // send to server console too
+			FOR_EACH_REALCLIENT(head) 
+				if(head != source)
+				if(head.active_minigame == source.active_minigame)
+					sprint(head, msgstr);
+		}
 		else if(teamsay > 0) // team message, only sent to team mates
 		{
 			sprint(source, sourcemsgstr);
diff --git a/qcsrc/server/command/cmd.qc b/qcsrc/server/command/cmd.qc
index 4a8b59eba6..7b74e48bf9 100644
--- a/qcsrc/server/command/cmd.qc
+++ b/qcsrc/server/command/cmd.qc
@@ -772,6 +772,7 @@ void ClientCommand_(float request)
 	CLIENT_COMMAND("suggestmap", ClientCommand_suggestmap(request, arguments), "Suggest a map to the mapvote at match end") \
 	CLIENT_COMMAND("tell", ClientCommand_tell(request, arguments, command), "Send a message directly to a player") \
 	CLIENT_COMMAND("voice", ClientCommand_voice(request, arguments, command), "Send voice message via sound") \
+	CLIENT_COMMAND("minigame", ClientCommand_minigame(request, arguments, command), "Start a minigame") \
 	/* nothing */
 
 void ClientCommand_macro_help()
diff --git a/qcsrc/server/g_world.qc b/qcsrc/server/g_world.qc
index 0a2708340d..7b427c4562 100644
--- a/qcsrc/server/g_world.qc
+++ b/qcsrc/server/g_world.qc
@@ -606,6 +606,8 @@ void spawnfunc_worldspawn (void)
 	CALL_ACCUMULATED_FUNCTION(RegisterDeathtypes);
 	CALL_ACCUMULATED_FUNCTION(RegisterBuffs);
 
+	initialize_minigames();
+
 	ServerProgsDB = db_load(strcat("server.db", autocvar_sessionid));
 
 	TemporaryDB = db_create();
diff --git a/qcsrc/server/progs.src b/qcsrc/server/progs.src
index 211af8a746..821fab9df6 100644
--- a/qcsrc/server/progs.src
+++ b/qcsrc/server/progs.src
@@ -94,6 +94,8 @@ weapons/weaponsystem.qc
 ../common/monsters/monsters.qc
 ../common/monsters/spawn.qc
 ../common/monsters/sv_monsters.qc
+../common/minigames/minigames.qc
+../common/minigames/sv_minigames.qc
 ../common/nades.qc
 ../common/net_notice.qc
 ../common/notifications.qc