about summary refs log tree commit diff
path: root/src
diff options
context:
space:
mode:
authoroy <Tom_Adams@web.de>2011-03-23 13:06:35 +0100
committeroy <Tom_Adams@web.de>2011-03-23 13:06:35 +0100
commitae325c873f64603f3957b3ded623a67088752ba8 (patch)
tree47f5abde2090975fc491503d0ecd5386da53830c /src
parentb64db75a6b5e6a08c356278ab73d29707579844c (diff)
downloadzcatch-ae325c873f64603f3957b3ded623a67088752ba8.tar.gz
zcatch-ae325c873f64603f3957b3ded623a67088752ba8.zip
added friends feature. Closes #24
Diffstat (limited to 'src')
-rw-r--r--src/engine/client/client.cpp3
-rw-r--r--src/engine/client/client.h1
-rw-r--r--src/engine/client/friends.cpp130
-rw-r--r--src/engine/client/friends.h32
-rw-r--r--src/engine/client/srvbrowse.cpp162
-rw-r--r--src/engine/client/srvbrowse.h1
-rw-r--r--src/engine/friends.h36
-rw-r--r--src/engine/shared/config_variables.h1
-rw-r--r--src/game/client/components/menus.cpp44
-rw-r--r--src/game/client/components/menus.h4
-rw-r--r--src/game/client/components/menus_browser.cpp111
-rw-r--r--src/game/client/components/menus_ingame.cpp17
-rw-r--r--src/game/client/gameclient.cpp11
-rw-r--r--src/game/client/gameclient.h3
-rw-r--r--src/game/variables.h2
15 files changed, 473 insertions, 85 deletions
diff --git a/src/engine/client/client.cpp b/src/engine/client/client.cpp
index cb477304..3131afc1 100644
--- a/src/engine/client/client.cpp
+++ b/src/engine/client/client.cpp
@@ -36,6 +36,7 @@
 #include <mastersrv/mastersrv.h>
 #include <versionsrv/versionsrv.h>
 
+#include "friends.h"
 #include "srvbrowse.h"
 #include "client.h"
 
@@ -1806,6 +1807,7 @@ void CClient::RegisterInterfaces()
 	Kernel()->RegisterInterface(static_cast<IDemoRecorder*>(&m_DemoRecorder));
 	Kernel()->RegisterInterface(static_cast<IDemoPlayer*>(&m_DemoPlayer));
 	Kernel()->RegisterInterface(static_cast<IServerBrowser*>(&m_ServerBrowser));
+	Kernel()->RegisterInterface(static_cast<IFriends*>(&m_Friends));
 }
 
 void CClient::InitInterfaces()
@@ -1823,6 +1825,7 @@ void CClient::InitInterfaces()
 
 	//
 	m_ServerBrowser.SetBaseInfo(&m_NetClient, m_pGameClient->NetVersion());
+	m_Friends.Init();
 }
 
 void CClient::Run()
diff --git a/src/engine/client/client.h b/src/engine/client/client.h
index b5be566b..31364cd2 100644
--- a/src/engine/client/client.h
+++ b/src/engine/client/client.h
@@ -105,6 +105,7 @@ class CClient : public IClient, public CDemoPlayer::IListner
 	class CDemoPlayer m_DemoPlayer;
 	class CDemoRecorder m_DemoRecorder;
 	class CServerBrowser m_ServerBrowser;
+	class CFriends m_Friends;
 
 	char m_aServerAddressStr[256];
 
diff --git a/src/engine/client/friends.cpp b/src/engine/client/friends.cpp
new file mode 100644
index 00000000..26d70892
--- /dev/null
+++ b/src/engine/client/friends.cpp
@@ -0,0 +1,130 @@
+/* (c) Magnus Auvinen. See licence.txt in the root of the distribution for more information. */
+/* If you are missing that file, acquire a complete release at teeworlds.com.                */
+#include <base/math.h>
+
+#include <engine/config.h>
+#include <engine/console.h>
+#include <engine/shared/config.h>
+
+#include "friends.h"
+
+CFriends::CFriends()
+{
+	mem_zero(m_aFriends, sizeof(m_aFriends));
+}
+
+void CFriends::ConAddFriend(IConsole::IResult *pResult, void *pUserData)
+{
+	CFriends *pSelf = (CFriends *)pUserData;
+	pSelf->AddFriend(pResult->GetString(0), pResult->GetString(1));
+}
+
+void CFriends::ConRemoveFriend(IConsole::IResult *pResult, void *pUserData)
+{
+	CFriends *pSelf = (CFriends *)pUserData;
+	pSelf->RemoveFriend(pResult->GetString(0), pResult->GetString(1));
+}
+
+void CFriends::Init()
+{
+	IConfig *pConfig = Kernel()->RequestInterface<IConfig>();
+	if(pConfig)
+		pConfig->RegisterCallback(ConfigSaveCallback, this);
+
+	IConsole *pConsole = Kernel()->RequestInterface<IConsole>();
+	if(pConsole)
+	{
+		pConsole->Register("add_friend", "ss", CFGFLAG_CLIENT, ConAddFriend, this, "Add a friend");
+		pConsole->Register("remove_Friend", "ss", CFGFLAG_CLIENT, ConRemoveFriend, this, "Remove a friend");
+	}
+}
+
+const CFriendInfo *CFriends::GetFriend(int Index) const
+{
+	return &m_aFriends[max(0, Index%m_NumFriends)];
+}
+
+bool CFriends::IsFriend(const char *pName, const char *pClan) const
+{
+	for(int i = 0; i < m_NumFriends; ++i)
+	{
+		if(!str_comp(m_aFriends[i].m_aName, pName) && !str_comp(m_aFriends[i].m_aClan, pClan))
+			return true;
+	}
+	return false;
+}
+
+void CFriends::AddFriend(const char *pName, const char *pClan)
+{
+	if(m_NumFriends == MAX_FRIENDS)
+		return;
+
+	// make sure we don't have the friend already
+	for(int i = 0; i < m_NumFriends; ++i)
+	{
+		if(!str_comp(m_aFriends[i].m_aName, pName) && !str_comp(m_aFriends[i].m_aClan, pClan))
+			return;
+	}
+
+	str_copy(m_aFriends[m_NumFriends].m_aName, pName, sizeof(m_aFriends[m_NumFriends].m_aName));
+	str_copy(m_aFriends[m_NumFriends].m_aClan, pClan, sizeof(m_aFriends[m_NumFriends].m_aClan));
+	++m_NumFriends;
+}
+
+void CFriends::RemoveFriend(const char *pName, const char *pClan)
+{
+	for(int i = 0; i < m_NumFriends; ++i)
+	{
+		if(!str_comp(m_aFriends[i].m_aName, pName) && !str_comp(m_aFriends[i].m_aClan, pClan))
+		{
+			RemoveFriend(i);
+			return;
+		}
+	}
+}
+
+void CFriends::RemoveFriend(int Index)
+{
+	if(Index >= 0 && Index < m_NumFriends)
+	{
+		mem_move(&m_aFriends[Index], &m_aFriends[Index+1], sizeof(CFriendInfo)*(m_NumFriends-(Index+1)));
+		--m_NumFriends;
+	}
+	return;
+}
+
+void CFriends::ConfigSaveCallback(IConfig *pConfig, void *pUserData)
+{
+	CFriends *pSelf = (CFriends *)pUserData;
+	char aBuf[128];
+	const char *pEnd = aBuf+sizeof(aBuf)-4;
+	for(int i = 0; i < pSelf->m_NumFriends; ++i)
+	{
+		str_copy(aBuf, "add_friend ", sizeof(aBuf));
+
+		const char *pSrc = pSelf->m_aFriends[i].m_aName;
+		char *pDst = aBuf+str_length(aBuf);
+		*pDst++ = '"';
+		while(*pSrc && pDst < pEnd)
+		{
+			if(*pSrc == '"' || *pSrc == '\\') // escape \ and "
+				*pDst++ = '\\';
+			*pDst++ = *pSrc++;
+		}
+		*pDst++ = '"';
+		*pDst++ = ' ';
+
+		pSrc = pSelf->m_aFriends[i].m_aClan;
+		*pDst++ = '"';
+		while(*pSrc && pDst < pEnd)
+		{
+			if(*pSrc == '"' || *pSrc == '\\') // escape \ and "
+				*pDst++ = '\\';
+			*pDst++ = *pSrc++;
+		}
+		*pDst++ = '"';
+		*pDst++ = 0;
+
+		pConfig->WriteLine(aBuf);
+	}
+}
diff --git a/src/engine/client/friends.h b/src/engine/client/friends.h
new file mode 100644
index 00000000..e046fc20
--- /dev/null
+++ b/src/engine/client/friends.h
@@ -0,0 +1,32 @@
+/* (c) Magnus Auvinen. See licence.txt in the root of the distribution for more information. */
+/* If you are missing that file, acquire a complete release at teeworlds.com.                */
+#ifndef ENGINE_CLIENT_FRIENDS_H
+#define ENGINE_CLIENT_FRIENDS_H
+
+#include <engine/friends.h>
+
+class CFriends : public IFriends
+{
+	CFriendInfo m_aFriends[MAX_FRIENDS];
+	int m_NumFriends;
+
+	static void ConAddFriend(IConsole::IResult *pResult, void *pUserData);
+	static void ConRemoveFriend(IConsole::IResult *pResult, void *pUserData);
+
+	static void ConfigSaveCallback(IConfig *pConfig, void *pUserData);
+	
+public:
+	CFriends();
+	
+	void Init();
+	
+	int NumFriends() const { return m_NumFriends; }
+	const CFriendInfo *GetFriend(int Index) const; 
+	bool IsFriend(const char *pName, const char *pClan) const;
+
+	void AddFriend(const char *pName, const char *pClan);
+	void RemoveFriend(const char *pName, const char *pClan);
+	void RemoveFriend(int Index);
+};
+
+#endif
diff --git a/src/engine/client/srvbrowse.cpp b/src/engine/client/srvbrowse.cpp
index b7c20ae4..be14eaa2 100644
--- a/src/engine/client/srvbrowse.cpp
+++ b/src/engine/client/srvbrowse.cpp
@@ -4,14 +4,16 @@
 
 #include <base/math.h>
 #include <base/system.h>
-#include <engine/shared/network.h>
-#include <engine/shared/protocol.h>
+
 #include <engine/shared/config.h>
 #include <engine/shared/memheap.h>
+#include <engine/shared/network.h>
+#include <engine/shared/protocol.h>
 
-#include <engine/masterserver.h>
-#include <engine/console.h>
 #include <engine/config.h>
+#include <engine/console.h>
+#include <engine/friends.h>
+#include <engine/masterserver.h>
 
 #include <mastersrv/mastersrv.h>
 
@@ -65,6 +67,7 @@ void CServerBrowser::SetBaseInfo(class CNetClient *pClient, const char *pNetVers
 	str_copy(m_aNetVersion, pNetVersion, sizeof(m_aNetVersion));
 	m_pMasterServer = Kernel()->RequestInterface<IMasterServer>();
 	m_pConsole = Kernel()->RequestInterface<IConsole>();
+	m_pFriends = Kernel()->RequestInterface<IFriends>();
 	IConfig *pConfig = Kernel()->RequestInterface<IConfig>();
 	if(pConfig)
 		pConfig->RegisterCallback(ConfigSaveCallback, this);
@@ -140,79 +143,95 @@ void CServerBrowser::Filter()
 	for(i = 0; i < m_NumServers; i++)
 	{
 		int Filtered = 0;
+		bool FoundFriend = false;
 
-		if(g_Config.m_BrFilterEmpty && ((g_Config.m_BrFilterSpectators && m_ppServerlist[i]->m_Info.m_NumPlayers == 0) || m_ppServerlist[i]->m_Info.m_NumClients == 0))
-			Filtered = 1;
-		else if(g_Config.m_BrFilterFull && ((g_Config.m_BrFilterSpectators && m_ppServerlist[i]->m_Info.m_NumPlayers == m_ppServerlist[i]->m_Info.m_MaxPlayers) ||
-				m_ppServerlist[i]->m_Info.m_NumClients == m_ppServerlist[i]->m_Info.m_MaxClients))
-			Filtered = 1;
-		else if(g_Config.m_BrFilterPw && m_ppServerlist[i]->m_Info.m_Flags&SERVER_FLAG_PASSWORD)
-			Filtered = 1;
-		else if(g_Config.m_BrFilterPure &&
-			(str_comp(m_ppServerlist[i]->m_Info.m_aGameType, "DM") != 0 &&
-			str_comp(m_ppServerlist[i]->m_Info.m_aGameType, "TDM") != 0 &&
-			str_comp(m_ppServerlist[i]->m_Info.m_aGameType, "CTF") != 0))
+		if(g_Config.m_BrFilterFriends)
 		{
-			Filtered = 1;
-		}
-		else if(g_Config.m_BrFilterPureMap &&
-			!(str_comp(m_ppServerlist[i]->m_Info.m_aMap, "dm1") == 0 ||
-			str_comp(m_ppServerlist[i]->m_Info.m_aMap, "dm2") == 0 ||
-			str_comp(m_ppServerlist[i]->m_Info.m_aMap, "dm6") == 0 ||
-			str_comp(m_ppServerlist[i]->m_Info.m_aMap, "dm7") == 0 ||
-			str_comp(m_ppServerlist[i]->m_Info.m_aMap, "dm8") == 0 ||
-			str_comp(m_ppServerlist[i]->m_Info.m_aMap, "dm9") == 0 ||
-			str_comp(m_ppServerlist[i]->m_Info.m_aMap, "ctf1") == 0 ||
-			str_comp(m_ppServerlist[i]->m_Info.m_aMap, "ctf2") == 0 ||
-			str_comp(m_ppServerlist[i]->m_Info.m_aMap, "ctf3") == 0 ||
-			str_comp(m_ppServerlist[i]->m_Info.m_aMap, "ctf4") == 0 ||
-			str_comp(m_ppServerlist[i]->m_Info.m_aMap, "ctf5") == 0)
-		)
-		{
-			Filtered = 1;
+			for(p = 0; p < m_ppServerlist[i]->m_Info.m_NumClients; p++)
+			{
+				if(m_pFriends->IsFriend(m_ppServerlist[i]->m_Info.m_aClients[p].m_aName, m_ppServerlist[i]->m_Info.m_aClients[p].m_aClan))
+				{
+					FoundFriend = true;
+					break;
+				}
+			}
 		}
-		else if(g_Config.m_BrFilterPing < m_ppServerlist[i]->m_Info.m_Latency)
-			Filtered = 1;
-		else if(g_Config.m_BrFilterCompatversion && str_comp_num(m_ppServerlist[i]->m_Info.m_aVersion, m_aNetVersion, 3) != 0)
-			Filtered = 1;
-		else if(g_Config.m_BrFilterServerAddress[0] && !str_find_nocase(m_ppServerlist[i]->m_Info.m_aAddress, g_Config.m_BrFilterServerAddress))
-			Filtered = 1;
-		else if(g_Config.m_BrFilterGametype[0] && !str_find_nocase(m_ppServerlist[i]->m_Info.m_aGameType, g_Config.m_BrFilterGametype))
-			Filtered = 1;
-		else if(g_Config.m_BrFilterString[0] != 0)
-		{
-			int MatchFound = 0;
-
-			m_ppServerlist[i]->m_Info.m_QuickSearchHit = 0;
 
-			// match against server name
-			if(str_find_nocase(m_ppServerlist[i]->m_Info.m_aName, g_Config.m_BrFilterString))
+		if(!FoundFriend)
+		{
+			if(g_Config.m_BrFilterEmpty && ((g_Config.m_BrFilterSpectators && m_ppServerlist[i]->m_Info.m_NumPlayers == 0) || m_ppServerlist[i]->m_Info.m_NumClients == 0))
+				Filtered = 1;
+			else if(g_Config.m_BrFilterFull && ((g_Config.m_BrFilterSpectators && m_ppServerlist[i]->m_Info.m_NumPlayers == m_ppServerlist[i]->m_Info.m_MaxPlayers) ||
+					m_ppServerlist[i]->m_Info.m_NumClients == m_ppServerlist[i]->m_Info.m_MaxClients))
+				Filtered = 1;
+			else if(g_Config.m_BrFilterPw && m_ppServerlist[i]->m_Info.m_Flags&SERVER_FLAG_PASSWORD)
+				Filtered = 1;
+			else if(g_Config.m_BrFilterPure &&
+				(str_comp(m_ppServerlist[i]->m_Info.m_aGameType, "DM") != 0 &&
+				str_comp(m_ppServerlist[i]->m_Info.m_aGameType, "TDM") != 0 &&
+				str_comp(m_ppServerlist[i]->m_Info.m_aGameType, "CTF") != 0))
 			{
-				MatchFound = 1;
-				m_ppServerlist[i]->m_Info.m_QuickSearchHit |= IServerBrowser::QUICK_SERVERNAME;
+				Filtered = 1;
 			}
-
-			// match against players
-			for(p = 0; p < m_ppServerlist[i]->m_Info.m_NumPlayers; p++)
+			else if(g_Config.m_BrFilterPureMap &&
+				!(str_comp(m_ppServerlist[i]->m_Info.m_aMap, "dm1") == 0 ||
+				str_comp(m_ppServerlist[i]->m_Info.m_aMap, "dm2") == 0 ||
+				str_comp(m_ppServerlist[i]->m_Info.m_aMap, "dm6") == 0 ||
+				str_comp(m_ppServerlist[i]->m_Info.m_aMap, "dm7") == 0 ||
+				str_comp(m_ppServerlist[i]->m_Info.m_aMap, "dm8") == 0 ||
+				str_comp(m_ppServerlist[i]->m_Info.m_aMap, "dm9") == 0 ||
+				str_comp(m_ppServerlist[i]->m_Info.m_aMap, "ctf1") == 0 ||
+				str_comp(m_ppServerlist[i]->m_Info.m_aMap, "ctf2") == 0 ||
+				str_comp(m_ppServerlist[i]->m_Info.m_aMap, "ctf3") == 0 ||
+				str_comp(m_ppServerlist[i]->m_Info.m_aMap, "ctf4") == 0 ||
+				str_comp(m_ppServerlist[i]->m_Info.m_aMap, "ctf5") == 0)
+			)
 			{
-				if(str_find_nocase(m_ppServerlist[i]->m_Info.m_aClients[p].m_aName, g_Config.m_BrFilterString) ||
-					str_find_nocase(m_ppServerlist[i]->m_Info.m_aClients[p].m_aClan, g_Config.m_BrFilterString))
+				Filtered = 1;
+			}
+			else if(g_Config.m_BrFilterPing < m_ppServerlist[i]->m_Info.m_Latency)
+				Filtered = 1;
+			else if(g_Config.m_BrFilterCompatversion && str_comp_num(m_ppServerlist[i]->m_Info.m_aVersion, m_aNetVersion, 3) != 0)
+				Filtered = 1;
+			else if(g_Config.m_BrFilterServerAddress[0] && !str_find_nocase(m_ppServerlist[i]->m_Info.m_aAddress, g_Config.m_BrFilterServerAddress))
+				Filtered = 1;
+			else if(g_Config.m_BrFilterGametype[0] && !str_find_nocase(m_ppServerlist[i]->m_Info.m_aGameType, g_Config.m_BrFilterGametype))
+				Filtered = 1;
+			else if(g_Config.m_BrFilterString[0] != 0)
+			{
+				int MatchFound = 0;
+
+				m_ppServerlist[i]->m_Info.m_QuickSearchHit = 0;
+
+				// match against server name
+				if(str_find_nocase(m_ppServerlist[i]->m_Info.m_aName, g_Config.m_BrFilterString))
 				{
 					MatchFound = 1;
-					m_ppServerlist[i]->m_Info.m_QuickSearchHit |= IServerBrowser::QUICK_PLAYER;
-					break;
+					m_ppServerlist[i]->m_Info.m_QuickSearchHit |= IServerBrowser::QUICK_SERVERNAME;
 				}
-			}
 
-			// match against map
-			if(str_find_nocase(m_ppServerlist[i]->m_Info.m_aMap, g_Config.m_BrFilterString))
-			{
-				MatchFound = 1;
-				m_ppServerlist[i]->m_Info.m_QuickSearchHit |= IServerBrowser::QUICK_MAPNAME;
-			}
+				// match against players
+				for(p = 0; p < m_ppServerlist[i]->m_Info.m_NumClients; p++)
+				{
+					if(str_find_nocase(m_ppServerlist[i]->m_Info.m_aClients[p].m_aName, g_Config.m_BrFilterString) ||
+						str_find_nocase(m_ppServerlist[i]->m_Info.m_aClients[p].m_aClan, g_Config.m_BrFilterString))
+					{
+						MatchFound = 1;
+						m_ppServerlist[i]->m_Info.m_QuickSearchHit |= IServerBrowser::QUICK_PLAYER;
+						break;
+					}
+				}
 
-			if(!MatchFound)
-				Filtered = 1;
+				// match against map
+				if(str_find_nocase(m_ppServerlist[i]->m_Info.m_aMap, g_Config.m_BrFilterString))
+				{
+					MatchFound = 1;
+					m_ppServerlist[i]->m_Info.m_QuickSearchHit |= IServerBrowser::QUICK_MAPNAME;
+				}
+
+				if(!MatchFound)
+					Filtered = 1;
+			}
 		}
 
 		if(Filtered == 0)
@@ -226,12 +245,13 @@ int CServerBrowser::SortHash() const
 	i |= g_Config.m_BrFilterEmpty<<4;
 	i |= g_Config.m_BrFilterFull<<5;
 	i |= g_Config.m_BrFilterSpectators<<6;
-	i |= g_Config.m_BrFilterPw<<7;
-	i |= g_Config.m_BrSortOrder<<8;
-	i |= g_Config.m_BrFilterCompatversion<<9;
-	i |= g_Config.m_BrFilterPure<<10;
-	i |= g_Config.m_BrFilterPureMap<<11;
-	i |= g_Config.m_BrFilterPing<<17;
+	i |= g_Config.m_BrFilterFriends<<7;
+	i |= g_Config.m_BrFilterPw<<8;
+	i |= g_Config.m_BrSortOrder<<9;
+	i |= g_Config.m_BrFilterCompatversion<<10;
+	i |= g_Config.m_BrFilterPure<<11;
+	i |= g_Config.m_BrFilterPureMap<<12;
+	i |= g_Config.m_BrFilterPing<<18;
 	return i;
 }
 
diff --git a/src/engine/client/srvbrowse.h b/src/engine/client/srvbrowse.h
index b3d0f9ed..876627bf 100644
--- a/src/engine/client/srvbrowse.h
+++ b/src/engine/client/srvbrowse.h
@@ -55,6 +55,7 @@ private:
 	CNetClient *m_pNetClient;
 	IMasterServer *m_pMasterServer;
 	class IConsole *m_pConsole;
+	class IFriends *m_pFriends;
 	char m_aNetVersion[128];
 
 	CHeap m_ServerlistHeap;
diff --git a/src/engine/friends.h b/src/engine/friends.h
new file mode 100644
index 00000000..d0b9e97b
--- /dev/null
+++ b/src/engine/friends.h
@@ -0,0 +1,36 @@
+/* (c) Magnus Auvinen. See licence.txt in the root of the distribution for more information. */
+/* If you are missing that file, acquire a complete release at teeworlds.com.                */
+#ifndef ENGINE_FRIENDS_H
+#define ENGINE_FRIENDS_H
+
+#include <engine/shared/protocol.h>
+
+#include "kernel.h"
+
+struct CFriendInfo
+{
+	char m_aName[MAX_NAME_LENGTH];
+	char m_aClan[MAX_CLAN_LENGTH];
+};
+
+class IFriends : public IInterface
+{
+	MACRO_INTERFACE("friends", 0)
+public:
+	enum
+	{
+		MAX_FRIENDS=128,
+	};
+	
+	virtual void Init() = 0;
+	
+	virtual int NumFriends() const = 0;
+	virtual const CFriendInfo *GetFriend(int Index) const = 0; 
+	virtual bool IsFriend(const char *pName, const char *pClan) const = 0;
+
+	virtual void AddFriend(const char *pName, const char *pClan) = 0;
+	virtual void RemoveFriend(const char *pName, const char *pClan) = 0;
+	virtual void RemoveFriend(int Index) = 0;
+};
+
+#endif
diff --git a/src/engine/shared/config_variables.h b/src/engine/shared/config_variables.h
index b95839ba..0e3dc8e3 100644
--- a/src/engine/shared/config_variables.h
+++ b/src/engine/shared/config_variables.h
@@ -31,6 +31,7 @@ MACRO_CONFIG_STR(BrFilterString, br_filter_string, 25, "", CFGFLAG_SAVE|CFGFLAG_
 MACRO_CONFIG_INT(BrFilterFull, br_filter_full, 0, 0, 1, CFGFLAG_SAVE|CFGFLAG_CLIENT, "Filter out full server in browser")
 MACRO_CONFIG_INT(BrFilterEmpty, br_filter_empty, 0, 0, 1, CFGFLAG_SAVE|CFGFLAG_CLIENT, "Filter out empty server in browser")
 MACRO_CONFIG_INT(BrFilterSpectators, br_filter_spectators, 0, 0, 1, CFGFLAG_SAVE|CFGFLAG_CLIENT, "Filter out spectators from player numbers")
+MACRO_CONFIG_INT(BrFilterFriends, br_filter_friends, 1, 0, 1, CFGFLAG_SAVE|CFGFLAG_CLIENT, "Do not filter out servers with friends")
 MACRO_CONFIG_INT(BrFilterPw, br_filter_pw, 0, 0, 1, CFGFLAG_SAVE|CFGFLAG_CLIENT, "Filter out password protected servers in browser")
 MACRO_CONFIG_INT(BrFilterPing, br_filter_ping, 999, 0, 999, CFGFLAG_SAVE|CFGFLAG_CLIENT, "Ping to filter by in the server browser")
 MACRO_CONFIG_STR(BrFilterGametype, br_filter_gametype, 128, "", CFGFLAG_SAVE|CFGFLAG_CLIENT, "Game types to filter")
diff --git a/src/game/client/components/menus.cpp b/src/game/client/components/menus.cpp
index cd8b7760..7264e412 100644
--- a/src/game/client/components/menus.cpp
+++ b/src/game/client/components/menus.cpp
@@ -6,10 +6,9 @@
 #include <base/math.h>
 #include <base/vmath.h>
 
-#include "menus.h"
-#include "skins.h"
-
+#include <engine/config.h>
 #include <engine/editor.h>
+#include <engine/friends.h>
 #include <engine/graphics.h>
 #include <engine/keys.h>
 #include <engine/serverbrowser.h>
@@ -26,6 +25,9 @@
 #include <game/localization.h>
 #include <mastersrv/mastersrv.h>
 
+#include "menus.h"
+#include "skins.h"
+
 vec4 CMenus::ms_GuiColor;
 vec4 CMenus::ms_ColorTabbarInactiveOutgame;
 vec4 CMenus::ms_ColorTabbarActiveOutgame;
@@ -64,6 +66,8 @@ CMenus::CMenus()
 	
 	str_copy(m_aCurrentDemoFolder, "demos", sizeof(m_aCurrentDemoFolder));
 	m_aCallvoteReason[0] = 0;
+
+	m_FriendlistSelectedIndex = -1;
 }
 
 vec4 CMenus::ButtonColorMul(const void *pID)
@@ -873,6 +877,12 @@ int CMenus::Render()
 			pExtraText = "";
 			ExtraAlign = -1;
 		}
+		else if(m_Popup == POPUP_REMOVE_FRIEND)
+		{
+			pTitle = Localize("Remove friend");
+			pExtraText = Localize("Are you sure that you want to remove the player from your friends list?");
+			ExtraAlign = -1;
+		}
 		else if(m_Popup == POPUP_SOUNDERROR)
 		{
 			pTitle = Localize("Sound error");
@@ -1145,6 +1155,34 @@ int CMenus::Render()
 			static float Offset = 0.0f;
 			DoEditBox(&Offset, &TextBox, m_aCurrentDemoFile, sizeof(m_aCurrentDemoFile), 12.0f, &Offset);
 		}
+		else if(m_Popup == POPUP_REMOVE_FRIEND)
+		{
+			CUIRect Yes, No;
+			Box.HSplitBottom(20.f, &Box, &Part);
+			Box.HSplitBottom(24.f, &Box, &Part);
+			Part.VMargin(80.0f, &Part);
+			
+			Part.VSplitMid(&No, &Yes);
+			
+			Yes.VMargin(20.0f, &Yes);
+			No.VMargin(20.0f, &No);
+
+			static int s_ButtonAbort = 0;
+			if(DoButton_Menu(&s_ButtonAbort, Localize("No"), 0, &No) || m_EscapePressed)
+				m_Popup = POPUP_NONE;
+
+			static int s_ButtonTryAgain = 0;
+			if(DoButton_Menu(&s_ButtonTryAgain, Localize("Yes"), 0, &Yes) || m_EnterPressed)
+			{
+				m_Popup = POPUP_NONE;
+				// remove friend
+				if(m_FriendlistSelectedIndex >= 0)
+				{
+					m_pClient->Friends()->RemoveFriend(m_FriendlistSelectedIndex);
+					Client()->ServerBrowserUpdate();
+				}
+			}
+		}
 		else if(m_Popup == POPUP_FIRST_LAUNCH)
 		{
 			CUIRect Label, TextBox;
diff --git a/src/game/client/components/menus.h b/src/game/client/components/menus.h
index e8236f59..3cdfe3f0 100644
--- a/src/game/client/components/menus.h
+++ b/src/game/client/components/menus.h
@@ -101,6 +101,7 @@ class CMenus : public CComponent
 		POPUP_LANGUAGE,
 		POPUP_DELETE_DEMO,
 		POPUP_RENAME_DEMO,
+		POPUP_REMOVE_FRIEND,
 		POPUP_SOUNDERROR,
 		POPUP_PASSWORD,
 		POPUP_QUIT, 
@@ -198,6 +199,8 @@ class CMenus : public CComponent
 	void DemolistOnUpdate(bool Reset);
 	void DemolistPopulate();
 	static int DemolistFetchCallback(const char *pName, int IsDir, int StorageType, void *pUser);
+
+	int m_FriendlistSelectedIndex;
 	
 	// found in menus.cpp
 	int Render();
@@ -222,6 +225,7 @@ class CMenus : public CComponent
 	void RenderServerbrowserServerList(CUIRect View);
 	void RenderServerbrowserServerDetail(CUIRect View);
 	void RenderServerbrowserFilters(CUIRect View);
+	void RenderServerbrowserFriends(CUIRect View);
 	void RenderServerbrowser(CUIRect MainView);
 	static void ConchainServerbrowserUpdate(IConsole::IResult *pResult, void *pUserData, IConsole::FCommandCallback pfnCallback, void *pCallbackUserData);
 	
diff --git a/src/game/client/components/menus_browser.cpp b/src/game/client/components/menus_browser.cpp
index 952d596d..01772935 100644
--- a/src/game/client/components/menus_browser.cpp
+++ b/src/game/client/components/menus_browser.cpp
@@ -1,5 +1,7 @@
 /* (c) Magnus Auvinen. See licence.txt in the root of the distribution for more information. */
 /* If you are missing that file, acquire a complete release at teeworlds.com.                */
+#include <engine/config.h>
+#include <engine/friends.h>
 #include <engine/graphics.h>
 #include <engine/keys.h>
 #include <engine/serverbrowser.h>
@@ -435,7 +437,7 @@ void CMenus::RenderServerbrowserFilters(CUIRect View)
 {
 	CUIRect ServerFilter = View, FilterHeader;
 	const float FontSize = 12.0f;
-	ServerFilter.HSplitBottom(5.0f, &ServerFilter, 0);
+	ServerFilter.HSplitBottom(42.5f, &ServerFilter, 0);
 
 	// server filter
 	ServerFilter.HSplitTop(ms_ListheaderHeight, &FilterHeader, &ServerFilter);
@@ -461,6 +463,10 @@ void CMenus::RenderServerbrowserFilters(CUIRect View)
 		g_Config.m_BrFilterFull ^= 1;
 
 	ServerFilter.HSplitTop(20.0f, &Button, &ServerFilter);
+	if (DoButton_CheckBox(&g_Config.m_BrFilterFriends, Localize("Show friends"), g_Config.m_BrFilterFriends, &Button))
+		g_Config.m_BrFilterFriends ^= 1;
+
+	ServerFilter.HSplitTop(20.0f, &Button, &ServerFilter);
 	if (DoButton_CheckBox(&g_Config.m_BrFilterPw, Localize("No password"), g_Config.m_BrFilterPw, &Button))
 		g_Config.m_BrFilterPw ^= 1;
 
@@ -536,8 +542,8 @@ void CMenus::RenderServerbrowserServerDetail(CUIRect View)
 	const CServerInfo *pSelectedServer = ServerBrowser()->SortedGet(m_SelectedIndex);
 
 	// split off a piece to use for scoreboard
-	ServerDetails.HSplitTop(100.0f, &ServerDetails, &ServerScoreBoard);
-	ServerDetails.HSplitBottom(5.0f, &ServerDetails, 0x0);
+	ServerDetails.HSplitTop(90.0f, &ServerDetails, &ServerScoreBoard);
+	ServerDetails.HSplitBottom(2.5f, &ServerDetails, 0x0);
 
 	// server details
 	CTextCursor Cursor;
@@ -605,7 +611,7 @@ void CMenus::RenderServerbrowserServerDetail(CUIRect View)
 	}
 
 	// server scoreboard
-	ServerScoreBoard.HSplitBottom(10.0f, &ServerScoreBoard, 0x0);
+	ServerScoreBoard.HSplitBottom(20.0f, &ServerScoreBoard, 0x0);
 	ServerScoreBoard.HSplitTop(ms_ListheaderHeight, &ServerHeader, &ServerScoreBoard);
 	RenderTools()->DrawUIRect(&ServerHeader, vec4(1,1,1,0.25f), CUI::CORNER_T, 4.0f);
 	RenderTools()->DrawUIRect(&ServerScoreBoard, vec4(0,0,0,0.15f), CUI::CORNER_B, 4.0f);
@@ -689,6 +695,88 @@ void CMenus::RenderServerbrowserServerDetail(CUIRect View)
 	}
 }
 
+void CMenus::RenderServerbrowserFriends(CUIRect View)
+{
+	CUIRect ServerFriends = View, FilterHeader;
+	const float FontSize = 12.0f;
+
+	// header
+	ServerFriends.HSplitTop(ms_ListheaderHeight, &FilterHeader, &ServerFriends);
+	RenderTools()->DrawUIRect(&FilterHeader, vec4(1,1,1,0.25f), CUI::CORNER_T, 4.0f);
+	RenderTools()->DrawUIRect(&ServerFriends, vec4(0,0,0,0.15f), 0, 4.0f);
+	UI()->DoLabelScaled(&FilterHeader, Localize("Friends"), FontSize+2.0f, 0);
+	CUIRect Button, List;
+
+	ServerFriends.VSplitLeft(5.0f, 0, &ServerFriends);
+	ServerFriends.Margin(3.0f, &ServerFriends);
+	ServerFriends.VMargin(5.0f, &ServerFriends);
+	ServerFriends.HSplitBottom(100.0f, &List, &ServerFriends);
+
+	// friends list(remove friend)
+	static int s_FriendList = 0;
+	static float s_ScrollValue = 0;
+	UiDoListboxStart(&s_FriendList, &List, 40.0f, "", "", m_pClient->Friends()->NumFriends(), 1, m_FriendlistSelectedIndex, s_ScrollValue);
+	
+	for(int i = 0; i < m_pClient->Friends()->NumFriends(); ++i)
+	{
+		const CFriendInfo *pFriend = m_pClient->Friends()->GetFriend(i);
+		CListboxItem Item = UiDoListboxNextItem(pFriend);
+		
+		if(Item.m_Visible)
+		{
+			Item.m_Rect.Margin(2.5f, &Item.m_Rect);
+			RenderTools()->DrawUIRect(&Item.m_Rect, vec4(1.0f, 1.0f, 1.0f, 0.1f), CUI::CORNER_ALL, 4.0f);
+			Item.m_Rect.Margin(2.5f, &Item.m_Rect);
+			Item.m_Rect.HSplitTop(14.0f, &Item.m_Rect, &Button);
+			UI()->DoLabelScaled(&Item.m_Rect, pFriend->m_aName, FontSize, -1);
+			UI()->DoLabelScaled(&Button, pFriend->m_aClan, FontSize, -1);
+		}
+	}
+	
+	m_FriendlistSelectedIndex = UiDoListboxEnd(&s_ScrollValue, 0);
+
+	ServerFriends.HSplitTop(2.5f, 0, &ServerFriends);
+	ServerFriends.HSplitTop(20.0f, &Button, &ServerFriends);
+	if(m_FriendlistSelectedIndex != -1)
+	{
+		static int s_RemoveButton = 0;
+		if(DoButton_Menu(&s_RemoveButton, Localize("Remove"), 0, &Button))
+			m_Popup = POPUP_REMOVE_FRIEND;
+	}
+
+	// add friend
+	if(m_pClient->Friends()->NumFriends() < IFriends::MAX_FRIENDS)
+	{
+		ServerFriends.HSplitTop(10.0f, 0, &ServerFriends);
+		ServerFriends.HSplitTop(19.0f, &Button, &ServerFriends);
+		char aBuf[64];
+		str_format(aBuf, sizeof(aBuf), "%s:", Localize("Name"));
+		UI()->DoLabelScaled(&Button, aBuf, FontSize, -1);
+		Button.VSplitLeft(80.0f, 0, &Button);
+		static char s_aName[MAX_NAME_LENGTH] = {0};
+		static float s_OffsetName = 0.0f;
+		DoEditBox(&s_aName, &Button, s_aName, sizeof(s_aName), FontSize, &s_OffsetName);
+
+		ServerFriends.HSplitTop(3.0f, 0, &ServerFriends);
+		ServerFriends.HSplitTop(19.0f, &Button, &ServerFriends);
+		str_format(aBuf, sizeof(aBuf), "%s:", Localize("Clan"));
+		UI()->DoLabelScaled(&Button, aBuf, FontSize, -1);
+		Button.VSplitLeft(80.0f, 0, &Button);
+		static char s_aClan[MAX_CLAN_LENGTH] = {0};
+		static float s_OffsetClan = 0.0f;
+		DoEditBox(&s_aClan, &Button, s_aClan, sizeof(s_aClan), FontSize, &s_OffsetClan);
+
+		ServerFriends.HSplitTop(3.0f, 0, &ServerFriends);
+		ServerFriends.HSplitTop(20.0f, &Button, &ServerFriends);
+		static int s_RemoveButton = 0;
+		if(DoButton_Menu(&s_RemoveButton, Localize("Add Friend"), 0, &Button))
+		{
+			m_pClient->Friends()->AddFriend(s_aName, s_aClan);
+			Client()->ServerBrowserUpdate();
+		}
+	}
+}
+
 void CMenus::RenderServerbrowser(CUIRect MainView)
 {
 	/*
@@ -723,11 +811,13 @@ void CMenus::RenderServerbrowser(CUIRect MainView)
 
 	// tab bar
 	{
-		CUIRect TabButton0, TabButton1;
+		CUIRect TabButton0, TabButton1, TabButton2;
 		TabBar.HSplitTop(5.0f, 0, &TabBar);
 		TabBar.HSplitTop(20.0f, &TabButton0, &TabBar);
-		TabBar.HSplitTop(5.0f, 0, &TabBar);
-		TabBar.HSplitTop(20.0f, &TabButton1, 0);
+		TabBar.HSplitTop(2.5f, 0, &TabBar);
+		TabBar.HSplitTop(20.0f, &TabButton1, &TabBar);
+		TabBar.HSplitTop(2.5f, 0, &TabBar);
+		TabBar.HSplitTop(20.0f, &TabButton2, 0);
 		vec4 Active = ms_ColorTabbarActive;
 		vec4 InActive = ms_ColorTabbarInactive;
 		ms_ColorTabbarActive = vec4(0.0f, 0.0f, 0.0f, 0.3f);
@@ -741,6 +831,10 @@ void CMenus::RenderServerbrowser(CUIRect MainView)
 		if (DoButton_MenuTab(&s_InfoTab, Localize("Info"), ToolboxPage==1, &TabButton1, CUI::CORNER_L))
 			ToolboxPage = 1;
 
+		static int s_FriendsTab = 0;
+		if (DoButton_MenuTab(&s_FriendsTab, Localize("Friends"), ToolboxPage==2, &TabButton2, CUI::CORNER_L))
+			ToolboxPage = 2;
+
 		ms_ColorTabbarActive = Active;
 		ms_ColorTabbarInactive = InActive;
 		g_Config.m_UiToolboxPage = ToolboxPage;
@@ -755,7 +849,8 @@ void CMenus::RenderServerbrowser(CUIRect MainView)
 			RenderServerbrowserFilters(ToolBox);
 		else if(ToolboxPage == 1)
 			RenderServerbrowserServerDetail(ToolBox);
-
+		else if(ToolboxPage == 2)
+			RenderServerbrowserFriends(ToolBox);
 	}
 
 	// status box
diff --git a/src/game/client/components/menus_ingame.cpp b/src/game/client/components/menus_ingame.cpp
index 2e7c33e8..184a9768 100644
--- a/src/game/client/components/menus_ingame.cpp
+++ b/src/game/client/components/menus_ingame.cpp
@@ -2,7 +2,9 @@
 /* If you are missing that file, acquire a complete release at teeworlds.com.                */
 #include <base/math.h>
 
+#include <engine/config.h>
 #include <engine/demo.h>
+#include <engine/friends.h>
 #include <engine/serverbrowser.h>
 #include <engine/textrender.h>
 #include <engine/shared/config.h>
@@ -113,7 +115,7 @@ void CMenus::RenderGame(CUIRect MainView)
 	UI()->DoLabelScaled(&Button, Localize("Player options"), 34.0f, -1);
 
 	CUIRect Player;
-	static int s_aPlayerIDs[MAX_CLIENTS] = {0};
+	static int s_aPlayerIDs[MAX_CLIENTS][2] = {0};
 	for(int i = 0; i < MAX_CLIENTS; ++i)
 	{
 		if(!m_pClient->m_Snap.m_paPlayerInfos[i] || i == m_pClient->m_Snap.m_LocalClientID)
@@ -142,8 +144,19 @@ void CMenus::RenderGame(CUIRect MainView)
 		// ignore button
 		ButtonBar.VSplitMid(&Button, &ButtonBar);
 		Button.VSplitRight(10.0f, &Button, 0);
-		if(DoButton_CheckBox(&s_aPlayerIDs[i], Localize("Ignore"), m_pClient->m_aClients[i].m_ChatIgnore, &Button))
+		if(DoButton_CheckBox(&s_aPlayerIDs[i][0], Localize("Ignore"), m_pClient->m_aClients[i].m_ChatIgnore, &Button))
 			m_pClient->m_aClients[i].m_ChatIgnore ^= 1;
+
+		// friend button
+		ButtonBar.VSplitMid(&Button, &ButtonBar);
+		Button.VSplitLeft(10.0f, 0, &Button);
+		if(DoButton_CheckBox(&s_aPlayerIDs[i][1], Localize("Friend"), m_pClient->m_aClients[i].m_Friend, &Button))
+		{
+			if(m_pClient->m_aClients[i].m_Friend)
+				m_pClient->Friends()->RemoveFriend(m_pClient->m_aClients[i].m_aName, m_pClient->m_aClients[i].m_aClan);
+			else
+				m_pClient->Friends()->AddFriend(m_pClient->m_aClients[i].m_aName, m_pClient->m_aClients[i].m_aClan);
+		}
 	}
 	
 	/*
diff --git a/src/game/client/gameclient.cpp b/src/game/client/gameclient.cpp
index 9f0898fc..21180b40 100644
--- a/src/game/client/gameclient.cpp
+++ b/src/game/client/gameclient.cpp
@@ -2,6 +2,7 @@
 /* If you are missing that file, acquire a complete release at teeworlds.com.                */
 #include <engine/editor.h>
 #include <engine/engine.h>
+#include <engine/friends.h>
 #include <engine/graphics.h>
 #include <engine/textrender.h>
 #include <engine/demo.h>
@@ -115,6 +116,7 @@ void CGameClient::OnConsoleInit()
 	m_pDemoRecorder = Kernel()->RequestInterface<IDemoRecorder>();
 	m_pServerBrowser = Kernel()->RequestInterface<IServerBrowser>();
 	m_pEditor = Kernel()->RequestInterface<IEditor>();
+	m_pFriends = Kernel()->RequestInterface<IFriends>();
 	
 	// setup pointers
 	m_pBinds = &::gs_Binds;
@@ -834,6 +836,15 @@ void CGameClient::OnNewSnapshot()
 			m_aClients[i].Reset();
 	}
 
+	// update friend state
+	for(int i = 0; i < MAX_CLIENTS; ++i)
+	{
+		if(i == m_Snap.m_LocalClientID || !m_Snap.m_paPlayerInfos[i] || !Friends()->IsFriend(m_aClients[i].m_aName, m_aClients[i].m_aClan))
+			m_aClients[i].m_Friend = false;
+		else
+			m_aClients[i].m_Friend = true;
+	}
+
 	// sort player infos by score
 	mem_copy(m_Snap.m_paInfoByScore, m_Snap.m_paPlayerInfos, sizeof(m_Snap.m_paInfoByScore));
 	for(int k = 0; k < MAX_CLIENTS-1; k++) // ffs, bubblesort
diff --git a/src/game/client/gameclient.h b/src/game/client/gameclient.h
index 22caa740..5a045708 100644
--- a/src/game/client/gameclient.h
+++ b/src/game/client/gameclient.h
@@ -43,6 +43,7 @@ class CGameClient : public IGameClient
 	class IDemoRecorder *m_pDemoRecorder;
 	class IServerBrowser *m_pServerBrowser;
 	class IEditor *m_pEditor;
+	class IFriends *m_pFriends;
 	
 	CLayers m_Layers;
 	class CCollision m_Collision;
@@ -80,6 +81,7 @@ public:
 	class CLayers *Layers() { return &m_Layers; };
 	class CCollision *Collision() { return &m_Collision; };
 	class IEditor *Editor() { return m_pEditor; }
+	class IFriends *Friends() { return m_pFriends; }
 	
 	int NetobjNumCorrections() { return m_NetObjHandler.NumObjCorrections(); }
 	const char *NetobjCorrectedOn() { return m_NetObjHandler.CorrectedObjOn(); }
@@ -178,6 +180,7 @@ public:
 		float m_Angle;
 		bool m_Active;
 		bool m_ChatIgnore;
+		bool m_Friend;
 		
 		void UpdateRenderInfo();
 		void Reset();
diff --git a/src/game/variables.h b/src/game/variables.h
index c1d18c00..a0ac64c4 100644
--- a/src/game/variables.h
+++ b/src/game/variables.h
@@ -40,7 +40,7 @@ MACRO_CONFIG_INT(PlayerColorFeet, player_color_feet, 65408, 0, 0xFFFFFF, CFGFLAG
 MACRO_CONFIG_STR(PlayerSkin, player_skin, 24, "default", CFGFLAG_CLIENT|CFGFLAG_SAVE, "Player skin")
 
 MACRO_CONFIG_INT(UiPage, ui_page, 5, 0, 9, CFGFLAG_CLIENT|CFGFLAG_SAVE, "Interface page")
-MACRO_CONFIG_INT(UiToolboxPage, ui_toolbox_page, 0, 0, 1, CFGFLAG_CLIENT|CFGFLAG_SAVE, "Toolbox page")
+MACRO_CONFIG_INT(UiToolboxPage, ui_toolbox_page, 0, 0, 2, CFGFLAG_CLIENT|CFGFLAG_SAVE, "Toolbox page")
 MACRO_CONFIG_STR(UiServerAddress, ui_server_address, 25, "localhost:8303", CFGFLAG_CLIENT|CFGFLAG_SAVE, "Interface server address")
 MACRO_CONFIG_INT(UiScale, ui_scale, 100, 50, 150, CFGFLAG_CLIENT|CFGFLAG_SAVE, "Interface scale")