about summary refs log tree commit diff
diff options
context:
space:
mode:
authoroy <Tom_Adams@web.de>2011-03-10 10:08:14 +0100
committeroy <Tom_Adams@web.de>2011-03-10 10:08:14 +0100
commitee2f625754ca9f01be6732550f16098c5afa704a (patch)
tree9776edf90fac02d1bf03606dbc8b79fd071ed917
parent6205583d6411e300edd1ebf2eb170fbc8e7119e8 (diff)
downloadzcatch-ee2f625754ca9f01be6732550f16098c5afa704a.tar.gz
zcatch-ee2f625754ca9f01be6732550f16098c5afa704a.zip
added extended spectator mode. Closes #28
-rw-r--r--datasrc/network.py12
-rw-r--r--src/game/client/components/binds.cpp1
-rw-r--r--src/game/client/components/controls.cpp3
-rw-r--r--src/game/client/components/hud.cpp49
-rw-r--r--src/game/client/components/hud.h5
-rw-r--r--src/game/client/components/menus_settings.cpp5
-rw-r--r--src/game/client/components/spectator.cpp172
-rw-r--r--src/game/client/components/spectator.h37
-rw-r--r--src/game/client/gameclient.cpp28
-rw-r--r--src/game/client/gameclient.h5
-rw-r--r--src/game/server/entities/character.cpp2
-rw-r--r--src/game/server/gamecontext.cpp56
-rw-r--r--src/game/server/player.cpp39
-rw-r--r--src/game/server/player.h41
14 files changed, 389 insertions, 66 deletions
diff --git a/datasrc/network.py b/datasrc/network.py
index 3b1afa77..e30b1a9b 100644
--- a/datasrc/network.py
+++ b/datasrc/network.py
@@ -26,6 +26,8 @@ enum
 
 	FLAG_ATSTAND=-2,
 	FLAG_TAKEN,
+
+	SPEC_FREEVIEW=-1,
 };
 '''
 
@@ -173,6 +175,12 @@ Objects = [
 		NetIntAny("m_ColorBody"),
 		NetIntAny("m_ColorFeet"),
 	]),
+
+	NetObject("SpectatorInfo", [
+		NetIntRange("m_SpectatorID", 'SPEC_FREEVIEW', 'MAX_CLIENTS-1'),
+		NetIntAny("m_X"),
+		NetIntAny("m_Y"),
+	]),
 	
 	## Events
 	
@@ -273,6 +281,10 @@ Messages = [
 	NetMessage("Cl_SetTeam", [
 		NetIntRange("m_Team", 'TEAM_SPECTATORS', 'TEAM_BLUE'),
 	]),
+
+	NetMessage("Cl_SetSpectatorMode", [
+		NetIntRange("m_SpectatorID", 'SPEC_FREEVIEW', 'MAX_CLIENTS-1'),
+	]),
 	
 	NetMessage("Cl_StartInfo", [
 		NetStringStrict("m_pName"),
diff --git a/src/game/client/components/binds.cpp b/src/game/client/components/binds.cpp
index 7f473ef4..746389bd 100644
--- a/src/game/client/components/binds.cpp
+++ b/src/game/client/components/binds.cpp
@@ -99,6 +99,7 @@ void CBinds::SetDefaults()
 	Bind(KEY_MOUSE_1, "+fire");
 	Bind(KEY_MOUSE_2, "+hook");
 	Bind(KEY_LSHIFT, "+emote");
+	Bind(KEY_RSHIFT, "+spectate");
 
 	Bind('1', "+weapon1");
 	Bind('2', "+weapon2");
diff --git a/src/game/client/components/controls.cpp b/src/game/client/components/controls.cpp
index df9a6bf0..f67caa8e 100644
--- a/src/game/client/components/controls.cpp
+++ b/src/game/client/components/controls.cpp
@@ -201,6 +201,9 @@ void CControls::OnRender()
 	// update target pos
 	if(m_pClient->m_Snap.m_pGameInfoObj && !(m_pClient->m_Snap.m_pGameInfoObj->m_GameStateFlags&GAMESTATEFLAG_PAUSED || m_pClient->m_Snap.m_Spectate))
 		m_TargetPos = m_pClient->m_LocalCharacterPos + m_MousePos;
+
+	if(m_pClient->m_Snap.m_Spectate && m_pClient->m_Snap.m_pSpectatorInfo)
+		m_MousePos = m_pClient->m_Snap.m_SpectatorPos;
 }
 
 bool CControls::OnMouseMove(float x, float y)
diff --git a/src/game/client/components/hud.cpp b/src/game/client/components/hud.cpp
index ca100f9d..aff76bc8 100644
--- a/src/game/client/components/hud.cpp
+++ b/src/game/client/components/hud.cpp
@@ -30,7 +30,6 @@ void CHud::OnReset()
 void CHud::RenderGameTimer()
 {
 	float Half = 300.0f*Graphics()->ScreenAspect()/2.0f;
-	Graphics()->MapScreen(0, 0, 300.0f*Graphics()->ScreenAspect(), 300.0f);
 	
 	if(!(m_pClient->m_Snap.m_pGameInfoObj->m_GameStateFlags&GAMESTATEFLAG_SUDDENDEATH))
 	{
@@ -340,8 +339,11 @@ void CHud::RenderCursor()
 	Graphics()->QuadsEnd();
 }
 
-void CHud::RenderHealthAndAmmo()
+void CHud::RenderHealthAndAmmo(const CNetObj_Character *pCharacter)
 {
+	if(!pCharacter)
+		return;
+
 	//mapscreen_to_group(gacenter_x, center_y, layers_game_group());
 
 	float x = 5;
@@ -351,15 +353,14 @@ void CHud::RenderHealthAndAmmo()
 	// render gui stuff
 
 	Graphics()->TextureSet(g_pData->m_aImages[IMAGE_GAME].m_Id);
-	Graphics()->MapScreen(0,0,m_Width,300);
 	
 	Graphics()->QuadsBegin();
 	
 	// if weaponstage is active, put a "glow" around the stage ammo
-	RenderTools()->SelectSprite(g_pData->m_Weapons.m_aId[m_pClient->m_Snap.m_pLocalCharacter->m_Weapon%NUM_WEAPONS].m_pSpriteProj);
+	RenderTools()->SelectSprite(g_pData->m_Weapons.m_aId[pCharacter->m_Weapon%NUM_WEAPONS].m_pSpriteProj);
 	IGraphics::CQuadItem Array[10];
 	int i;
-	for (i = 0; i < min(m_pClient->m_Snap.m_pLocalCharacter->m_AmmoCount, 10); i++)
+	for (i = 0; i < min(pCharacter->m_AmmoCount, 10); i++)
 		Array[i] = IGraphics::CQuadItem(x+i*12,y+24,10,10);
 	Graphics()->QuadsDrawTL(Array, i);
 	Graphics()->QuadsEnd();
@@ -369,7 +370,7 @@ void CHud::RenderHealthAndAmmo()
 
 	// render health
 	RenderTools()->SelectSprite(SPRITE_HEALTH_FULL);
-	for(; h < min(m_pClient->m_Snap.m_pLocalCharacter->m_Health, 10); h++)
+	for(; h < min(pCharacter->m_Health, 10); h++)
 		Array[h] = IGraphics::CQuadItem(x+h*12,y,10,10);
 	Graphics()->QuadsDrawTL(Array, h);
 
@@ -382,7 +383,7 @@ void CHud::RenderHealthAndAmmo()
 	// render armor meter
 	h = 0;
 	RenderTools()->SelectSprite(SPRITE_ARMOR_FULL);
-	for(; h < min(m_pClient->m_Snap.m_pLocalCharacter->m_Armor, 10); h++)
+	for(; h < min(pCharacter->m_Armor, 10); h++)
 		Array[h] = IGraphics::CQuadItem(x+h*12,y+12,10,10);
 	Graphics()->QuadsDrawTL(Array, h);
 
@@ -394,19 +395,39 @@ void CHud::RenderHealthAndAmmo()
 	Graphics()->QuadsEnd();
 }
 
+void CHud::RenderSpectatorHud()
+{
+	// draw the box
+	Graphics()->TextureSet(-1);
+	Graphics()->QuadsBegin();
+	Graphics()->SetColor(0.0f, 0.0f, 0.0f, 0.4f);
+	RenderTools()->DrawRoundRectExt(m_Width-180.0f, m_Height-15.0f, 180.0f, 15.0f, 5.0f, CUI::CORNER_TL);
+	Graphics()->QuadsEnd();
+
+	// draw the text
+	char aBuf[128];
+	str_format(aBuf, sizeof(aBuf), "%s: %s", Localize("Spectate"), m_pClient->m_Snap.m_pSpectatorInfo && m_pClient->m_Snap.m_pSpectatorInfo->m_SpectatorID != SPEC_FREEVIEW ?
+		m_pClient->m_aClients[m_pClient->m_Snap.m_pSpectatorInfo->m_SpectatorID].m_aName : Localize("Free-View"));
+	TextRender()->Text(0, m_Width-174.0f, m_Height-13.0f, 8.0f, aBuf, -1);
+}
+
 void CHud::OnRender()
 {
 	if(!m_pClient->m_Snap.m_pGameInfoObj)
 		return;
 		
-	m_Width = 300*Graphics()->ScreenAspect();
+	m_Width = 300.0f*Graphics()->ScreenAspect();
+	m_Height = 300.0f;
+	Graphics()->MapScreen(0.0f, 0.0f, m_Width, m_Height);
 
-	bool Spectate = false;
-	if(m_pClient->m_Snap.m_pLocalInfo && m_pClient->m_Snap.m_pLocalInfo->m_Team == TEAM_SPECTATORS)
-		Spectate = true;
-	
-	if(m_pClient->m_Snap.m_pLocalCharacter && !Spectate && !(m_pClient->m_Snap.m_pGameInfoObj->m_GameStateFlags&GAMESTATEFLAG_GAMEOVER))
-		RenderHealthAndAmmo();
+	if(m_pClient->m_Snap.m_pLocalCharacter && !(m_pClient->m_Snap.m_pGameInfoObj->m_GameStateFlags&GAMESTATEFLAG_GAMEOVER))
+		RenderHealthAndAmmo(m_pClient->m_Snap.m_pLocalCharacter);
+	else if(m_pClient->m_Snap.m_Spectate)
+	{
+		if(m_pClient->m_Snap.m_pSpectatorInfo && m_pClient->m_Snap.m_pSpectatorInfo->m_SpectatorID != SPEC_FREEVIEW)
+			RenderHealthAndAmmo(&m_pClient->m_Snap.m_aCharacters[m_pClient->m_Snap.m_pSpectatorInfo->m_SpectatorID].m_Cur);
+		RenderSpectatorHud();
+	}
 
 	RenderGameTimer();
 	RenderSuddenDeath();
diff --git a/src/game/client/components/hud.h b/src/game/client/components/hud.h
index f1f3dc0a..75702d51 100644
--- a/src/game/client/components/hud.h
+++ b/src/game/client/components/hud.h
@@ -6,7 +6,7 @@
 
 class CHud : public CComponent
 {	
-	float m_Width;
+	float m_Width, m_Height;
 	float m_AverageFPS;
 	
 	void RenderCursor();
@@ -15,10 +15,11 @@ class CHud : public CComponent
 	void RenderConnectionWarning();
 	void RenderTeambalanceWarning();
 	void RenderVoting();
-	void RenderHealthAndAmmo();
+	void RenderHealthAndAmmo(const CNetObj_Character *pCharacter);
 	void RenderGameTimer();
 	void RenderSuddenDeath();
 	void RenderScoreHud();
+	void RenderSpectatorHud();
 	void RenderWarmupTimer();
 
 	void MapscreenToGroup(float CenterX, float CenterY, struct CMapItemGroup *PGroup);
diff --git a/src/game/client/components/menus_settings.cpp b/src/game/client/components/menus_settings.cpp
index 9accd6af..4bdf0fd3 100644
--- a/src/game/client/components/menus_settings.cpp
+++ b/src/game/client/components/menus_settings.cpp
@@ -322,6 +322,7 @@ static CKeyInfo gs_aKeys[] =
 	{ "Team chat", "chat team", 0 },
 	{ "Show chat", "+show_chat", 0 },
 	{ "Emoticon", "+emote", 0 },
+	{ "Spectator mode", "+spectate", 0 },
 	{ "Console", "toggle_local_console", 0 },
 	{ "Remote console", "toggle_remote_console", 0 },
 	{ "Screenshot", "screenshot", 0 },
@@ -331,7 +332,7 @@ static CKeyInfo gs_aKeys[] =
 	Localize("Move left");Localize("Move right");Localize("Jump");Localize("Fire");Localize("Hook");Localize("Hammer");
 	Localize("Pistol");Localize("Shotgun");Localize("Grenade");Localize("Rifle");Localize("Next weapon");Localize("Prev. weapon");
 	Localize("Vote yes");Localize("Vote no");Localize("Chat");Localize("Team chat");Localize("Show chat");Localize("Emoticon");
-	Localize("Console");Localize("Remote console");Localize("Screenshot");Localize("Scoreboard");
+	Localize("Spectate");Localize("Console");Localize("Remote console");Localize("Screenshot");Localize("Scoreboard");
 */
 
 const int g_KeyCount = sizeof(gs_aKeys) / sizeof(CKeyInfo);
@@ -458,7 +459,7 @@ void CMenus::RenderSettingsControls(CUIRect MainView)
 		TextRender()->Text(0, MiscSettings.x, MiscSettings.y, 14.0f*UI()->Scale(), Localize("Miscellaneous"), -1);
 
 		MiscSettings.HSplitTop(14.0f+5.0f+10.0f, 0, &MiscSettings);
-		UiDoGetButtons(17, 22, MiscSettings);
+		UiDoGetButtons(17, 23, MiscSettings);
 	}
 
 	// defaults
diff --git a/src/game/client/components/spectator.cpp b/src/game/client/components/spectator.cpp
new file mode 100644
index 00000000..18035248
--- /dev/null
+++ b/src/game/client/components/spectator.cpp
@@ -0,0 +1,172 @@
+/* (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/graphics.h>
+#include <engine/textrender.h>
+#include <engine/shared/config.h>
+
+#include <game/generated/client_data.h>
+#include <game/generated/protocol.h>
+
+#include <game/client/animstate.h>
+#include <game/client/render.h>
+
+#include "spectator.h"
+
+
+void CSpectator::ConKeySpectator(IConsole::IResult *pResult, void *pUserData)
+{
+	CSpectator *pSelf = (CSpectator *)pUserData;
+	if(pSelf->m_pClient->m_Snap.m_Spectate)
+		pSelf->m_Active = pResult->GetInteger(0) != 0;
+}
+
+void CSpectator::ConSpectate(IConsole::IResult *pResult, void *pUserData)
+{
+	((CSpectator *)pUserData)->Spectate(pResult->GetInteger(0));
+}
+
+CSpectator::CSpectator()
+{
+	OnReset();
+}
+
+void CSpectator::OnConsoleInit()
+{
+	Console()->Register("+spectate", "", CFGFLAG_CLIENT, ConKeySpectator, this, "Open spectator mode selector");
+	Console()->Register("spectate", "i", CFGFLAG_CLIENT, ConSpectate, this, "Switch spectator mode");
+}
+
+bool CSpectator::OnMouseMove(float x, float y)
+{
+	if(!m_Active)
+		return false;
+	
+	m_SelectorMouse += vec2(x,y);
+	return true;
+}
+
+void CSpectator::OnRelease()
+{
+	OnReset();
+}
+	
+void CSpectator::OnRender()
+{
+	if(!m_Active)
+	{
+		if(m_WasActive)
+		{
+			if(m_SelectedSpectatorID != NO_SELECTION)
+				Spectate(m_SelectedSpectatorID);
+			OnReset();
+		}
+		return;
+	}
+	
+	m_WasActive = true;
+	m_SelectedSpectatorID = NO_SELECTION;
+
+	// draw background
+	float Width = 400*3.0f*Graphics()->ScreenAspect();
+	float Height = 400*3.0f;
+	
+	Graphics()->MapScreen(0, 0, Width, Height);
+
+	Graphics()->BlendNormal();
+	Graphics()->TextureSet(-1);
+	Graphics()->QuadsBegin();
+	Graphics()->SetColor(0.0f, 0.0f, 0.0f, 0.3f);
+	RenderTools()->DrawRoundRect(Width/2.0f-300.0f, Height/2.0f-300.0f, 600.0f, 600.0f, 20.0f);
+	Graphics()->QuadsEnd();
+
+	// clamp mouse position to selector area
+	m_SelectorMouse.x = clamp(m_SelectorMouse.x, -280.0f, 280.0f);
+	m_SelectorMouse.y = clamp(m_SelectorMouse.y, -280.0f, 280.0f);
+
+	// draw selections
+	float FontSize = 20.0f;
+	float StartY = -190.0f;
+	float LineHeight = 60.0f;
+	bool Selected  = false;
+
+	int SpectatorID = m_pClient->m_Snap.m_pSpectatorInfo ? m_pClient->m_Snap.m_pSpectatorInfo->m_SpectatorID : SPEC_FREEVIEW;
+	if(SpectatorID == SPEC_FREEVIEW)
+	{
+		Graphics()->TextureSet(-1);
+		Graphics()->QuadsBegin();
+		Graphics()->SetColor(1.0f, 1.0f, 1.0f, 0.25f);
+		RenderTools()->DrawRoundRect(Width/2.0f-280.0f, Height/2.0f-280.0f, 270.0f, 60.0f, 20.0f);
+		Graphics()->QuadsEnd();
+	}
+
+	if(m_SelectorMouse.x >= -280.0f && m_SelectorMouse.x <= -10.0f &&
+		m_SelectorMouse.y >= -280.0f && m_SelectorMouse.y <= -220.0f)
+	{
+		m_SelectedSpectatorID = SPEC_FREEVIEW;
+		Selected = true;
+	}
+	TextRender()->TextColor(1.0f, 1.0f, 1.0f, Selected?1.0f:0.5f);
+	TextRender()->Text(0, Width/2.0f-240.0f, Height/2.0f-265.0f, FontSize, Localize("Free-View"), -1);
+
+	float x = -270.0f, y = StartY;
+	for(int i = 0, Count = 0; i < MAX_CLIENTS; ++i)
+	{
+		if(!m_pClient->m_Snap.m_paPlayerInfos[i] || m_pClient->m_Snap.m_paPlayerInfos[i]->m_Team == TEAM_SPECTATORS)
+			continue;
+
+		if(++Count%9 == 0)
+		{
+			x += 290.0f;
+			y = StartY;
+		}
+
+		if(SpectatorID == i)
+		{
+			Graphics()->TextureSet(-1);
+			Graphics()->QuadsBegin();
+			Graphics()->SetColor(1.0f, 1.0f, 1.0f, 0.25f);
+			RenderTools()->DrawRoundRect(Width/2.0f+x-10.0f, Height/2.0f+y-10.0f, 270.0f, 60.0f, 20.0f);
+			Graphics()->QuadsEnd();
+		}
+
+		Selected = false;
+		if(m_SelectorMouse.x >= x-10.0f && m_SelectorMouse.x <= x+260.0f &&
+			m_SelectorMouse.y >= y-10.0f && m_SelectorMouse.y <= y+50.0f)
+		{
+			m_SelectedSpectatorID = i;
+			Selected = true;
+		}
+		TextRender()->TextColor(1.0f, 1.0f, 1.0f, Selected?1.0f:0.5f);
+		TextRender()->Text(0, Width/2.0f+x+50.0f, Height/2.0f+y+5.0f, FontSize, m_pClient->m_aClients[i].m_aName, 220.0f);
+
+		CTeeRenderInfo TeeInfo = m_pClient->m_aClients[i].m_RenderInfo;
+		RenderTools()->RenderTee(CAnimState::GetIdle(), &TeeInfo, EMOTE_NORMAL, vec2(1.0f, 0.0f), vec2(Width/2.0f+x+20.0f, Height/2.0f+y+20.0f));
+		
+		y += LineHeight;
+	}
+
+	// draw cursor
+	Graphics()->TextureSet(g_pData->m_aImages[IMAGE_CURSOR].m_Id);
+	Graphics()->QuadsBegin();
+	Graphics()->SetColor(1.0f, 1.0f, 1.0f, 1.0f);
+	IGraphics::CQuadItem QuadItem(m_SelectorMouse.x+Width/2.0f, m_SelectorMouse.y+Height/2.0f, 48.0f, 48.0f);
+	Graphics()->QuadsDrawTL(&QuadItem, 1);
+	Graphics()->QuadsEnd();
+}
+
+void CSpectator::OnReset()
+{
+	m_WasActive = false;
+	m_Active = false;
+	m_SelectedSpectatorID = NO_SELECTION;
+}
+
+void CSpectator::Spectate(int SpectatorID)
+{
+	if(m_pClient->m_Snap.m_pSpectatorInfo && m_pClient->m_Snap.m_pSpectatorInfo->m_SpectatorID == SpectatorID)
+		return;
+
+	CNetMsg_Cl_SetSpectatorMode Msg;
+	Msg.m_SpectatorID = SpectatorID;
+	Client()->SendPackMsg(&Msg, MSGFLAG_VITAL);
+}
diff --git a/src/game/client/components/spectator.h b/src/game/client/components/spectator.h
new file mode 100644
index 00000000..607780f5
--- /dev/null
+++ b/src/game/client/components/spectator.h
@@ -0,0 +1,37 @@
+/* (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 GAME_CLIENT_COMPONENTS_SPECTATOR_H
+#define GAME_CLIENT_COMPONENTS_SPECTATOR_H
+#include <base/vmath.h>
+
+#include <game/client/component.h>
+
+class CSpectator : public CComponent
+{
+	enum
+	{
+		NO_SELECTION=-2,
+	};
+
+	bool m_Active;
+	bool m_WasActive;
+	
+	int m_SelectedSpectatorID;
+	vec2 m_SelectorMouse;
+
+	static void ConKeySpectator(IConsole::IResult *pResult, void *pUserData);
+	static void ConSpectate(IConsole::IResult *pResult, void *pUserData);
+	
+public:
+	CSpectator();
+	
+	virtual void OnConsoleInit();
+	virtual bool OnMouseMove(float x, float y);
+	virtual void OnRender();
+	virtual void OnRelease();
+	virtual void OnReset();
+
+	void Spectate(int SpectatorID);
+};
+
+#endif
diff --git a/src/game/client/gameclient.cpp b/src/game/client/gameclient.cpp
index f80d0266..0d847162 100644
--- a/src/game/client/gameclient.cpp
+++ b/src/game/client/gameclient.cpp
@@ -44,6 +44,7 @@
 #include "components/scoreboard.h"
 #include "components/skins.h"
 #include "components/sounds.h"
+#include "components/spectator.h"
 #include "components/voting.h"
 
 CGameClient g_GameClient;
@@ -69,6 +70,7 @@ static CSounds gs_Sounds;
 static CEmoticon gs_Emoticon;
 static CDamageInd gsDamageInd;
 static CVoting gs_Voting;
+static CSpectator gs_Spectator;
 
 static CPlayers gs_Players;
 static CNamePlates gs_NamePlates;
@@ -139,6 +141,7 @@ void CGameClient::OnConsoleInit()
 	m_All.Add(&m_pParticles->m_RenderGeneral);
 	m_All.Add(m_pDamageind);
 	m_All.Add(&gs_Hud);
+	m_All.Add(&gs_Spectator);
 	m_All.Add(&gs_Emoticon);
 	m_All.Add(&gs_KillMessages);
 	m_All.Add(m_pChat);
@@ -156,6 +159,7 @@ void CGameClient::OnConsoleInit()
 	m_Input.Add(m_pChat); // chat has higher prio due to tha you can quit it by pressing esc
 	m_Input.Add(m_pMotd); // for pressing esc to remove it
 	m_Input.Add(m_pMenus);
+	m_Input.Add(&gs_Spectator);
 	m_Input.Add(&gs_Emoticon);
 	m_Input.Add(m_pControls);
 	m_Input.Add(m_pBinds);
@@ -336,8 +340,9 @@ void CGameClient::OnReset()
 }
 
 
-void CGameClient::UpdateLocalCharacterPos()
+void CGameClient::UpdatePositions()
 {
+	// local character position
 	if(g_Config.m_ClPredict && Client()->State() != IClient::STATE_DEMOPLAYBACK)
 	{
 		if(!m_Snap.m_pLocalCharacter || (m_Snap.m_pGameInfoObj && m_Snap.m_pGameInfoObj->m_GameStateFlags&GAMESTATEFLAG_GAMEOVER))
@@ -353,6 +358,16 @@ void CGameClient::UpdateLocalCharacterPos()
 			vec2(m_Snap.m_pLocalPrevCharacter->m_X, m_Snap.m_pLocalPrevCharacter->m_Y),
 			vec2(m_Snap.m_pLocalCharacter->m_X, m_Snap.m_pLocalCharacter->m_Y), Client()->IntraGameTick());
 	}
+
+	// spectator position
+	if(m_Snap.m_Spectate && m_Snap.m_pSpectatorInfo)
+	{
+		if(m_Snap.m_pPrevSpectatorInfo)
+			m_Snap.m_SpectatorPos = mix(vec2(m_Snap.m_pPrevSpectatorInfo->m_X, m_Snap.m_pPrevSpectatorInfo->m_Y),
+										vec2(m_Snap.m_pSpectatorInfo->m_X, m_Snap.m_pSpectatorInfo->m_Y), Client()->IntraGameTick());
+		else
+			m_Snap.m_SpectatorPos = vec2(m_Snap.m_pSpectatorInfo->m_X, m_Snap.m_pSpectatorInfo->m_Y);
+	}
 }
 
 
@@ -393,8 +408,8 @@ void CGameClient::OnRender()
 	
 	return;*/
 	
-	// update the local character position
-	UpdateLocalCharacterPos();
+	// update the local character and spectate position
+	UpdatePositions();
 	
 	// dispatch all input to systems
 	DispatchInput();
@@ -716,11 +731,11 @@ void CGameClient::OnNewSnapshot()
 			else if(Item.m_Type == NETOBJTYPE_CHARACTER)
 			{
 				const void *pOld = Client()->SnapFindItem(IClient::SNAP_PREV, NETOBJTYPE_CHARACTER, Item.m_ID);
+				m_Snap.m_aCharacters[Item.m_ID].m_Cur = *((const CNetObj_Character *)pData);
 				if(pOld)
 				{
 					m_Snap.m_aCharacters[Item.m_ID].m_Active = true;
 					m_Snap.m_aCharacters[Item.m_ID].m_Prev = *((const CNetObj_Character *)pOld);
-					m_Snap.m_aCharacters[Item.m_ID].m_Cur = *((const CNetObj_Character *)pData);
 
 					if(m_Snap.m_aCharacters[Item.m_ID].m_Prev.m_Tick)
 						Evolve(&m_Snap.m_aCharacters[Item.m_ID].m_Prev, Client()->PrevGameTick());
@@ -728,6 +743,11 @@ void CGameClient::OnNewSnapshot()
 						Evolve(&m_Snap.m_aCharacters[Item.m_ID].m_Cur, Client()->GameTick());
 				}
 			}
+			else if(Item.m_Type == NETOBJTYPE_SPECTATORINFO)
+			{
+				m_Snap.m_pSpectatorInfo = (const CNetObj_SpectatorInfo *)pData;
+				m_Snap.m_pPrevSpectatorInfo = (const CNetObj_SpectatorInfo *)Client()->SnapFindItem(IClient::SNAP_PREV, NETOBJTYPE_SPECTATORINFO, Item.m_ID);
+			}
 			else if(Item.m_Type == NETOBJTYPE_GAMEINFO)
 			{
 				static bool s_GameOver = 0;
diff --git a/src/game/client/gameclient.h b/src/game/client/gameclient.h
index 3681b301..5e8b9391 100644
--- a/src/game/client/gameclient.h
+++ b/src/game/client/gameclient.h
@@ -49,7 +49,7 @@ class CGameClient : public IGameClient
 	
 	void DispatchInput();
 	void ProcessEvents();
-	void UpdateLocalCharacterPos();
+	void UpdatePositions();
 
 	int m_PredictedTick;
 	int m_LastNewPredictedTick;
@@ -109,6 +109,8 @@ public:
 		const CNetObj_Character *m_pLocalCharacter;
 		const CNetObj_Character *m_pLocalPrevCharacter;
 		const CNetObj_PlayerInfo *m_pLocalInfo;
+		const CNetObj_SpectatorInfo *m_pSpectatorInfo;
+		const CNetObj_SpectatorInfo *m_pPrevSpectatorInfo;
 		const CNetObj_Flag *m_paFlags[2];
 		const CNetObj_GameInfo *m_pGameInfoObj;
 		const CNetObj_GameData *m_pGameDataObj;
@@ -121,6 +123,7 @@ public:
 		int m_NumPlayers;
 		int m_aTeamSize[2];
 		bool m_Spectate;
+		vec2 m_SpectatorPos;
 		
 		//
 		struct CCharacterInfo
diff --git a/src/game/server/entities/character.cpp b/src/game/server/entities/character.cpp
index ddd416d1..32a9523e 100644
--- a/src/game/server/entities/character.cpp
+++ b/src/game/server/entities/character.cpp
@@ -822,7 +822,7 @@ void CCharacter::Snap(int SnappingClient)
 
 	pCharacter->m_Direction = m_Input.m_Direction;
 
-	if(m_pPlayer->GetCID() == SnappingClient)
+	if(m_pPlayer->GetCID() == SnappingClient || SnappingClient == -1 || m_pPlayer->GetCID() == GameServer()->m_apPlayers[SnappingClient]->m_SpectatorID)
 	{
 		pCharacter->m_Health = m_Health;
 		pCharacter->m_Armor = m_Armor;
diff --git a/src/game/server/gamecontext.cpp b/src/game/server/gamecontext.cpp
index 44d9b6fc..7f8a075f 100644
--- a/src/game/server/gamecontext.cpp
+++ b/src/game/server/gamecontext.cpp
@@ -389,7 +389,10 @@ void CGameContext::OnTick()
 	for(int i = 0; i < MAX_CLIENTS; i++)
 	{
 		if(m_apPlayers[i])
+		{
 			m_apPlayers[i]->Tick();
+			m_apPlayers[i]->PostTick();
+		}
 	}
 	
 	// update voting
@@ -454,7 +457,7 @@ void CGameContext::OnTick()
 				SendChat(-1, CGameContext::CHAT_ALL, "Vote passed");
 			
 				if(m_apPlayers[m_VoteCreator])
-					m_apPlayers[m_VoteCreator]->m_Last_VoteCall = 0;
+					m_apPlayers[m_VoteCreator]->m_LastVoteCall = 0;
 			}
 			else if(m_VoteEnforce == VOTE_ENFORCE_NO || time_get() > m_VoteCloseTime)
 			{
@@ -548,6 +551,13 @@ void CGameContext::OnClientDrop(int ClientID)
 	
 	(void)m_pController->CheckTeamBalance();
 	m_VoteUpdate = true;
+
+	// update spectator modes
+	for(int i = 0; i < MAX_CLIENTS; ++i)
+	{
+		if(m_apPlayers[i] && m_apPlayers[i]->m_SpectatorID == ClientID)
+			m_apPlayers[i]->m_SpectatorID = SPEC_FREEVIEW;
+	}
 }
 
 void CGameContext::OnMessage(int MsgID, CUnpacker *pUnpacker, int ClientID)
@@ -572,10 +582,10 @@ void CGameContext::OnMessage(int MsgID, CUnpacker *pUnpacker, int ClientID)
 		else
 			Team = CGameContext::CHAT_ALL;
 		
-		if(g_Config.m_SvSpamprotection && pPlayer->m_Last_Chat && pPlayer->m_Last_Chat+Server()->TickSpeed() > Server()->Tick())
+		if(g_Config.m_SvSpamprotection && pPlayer->m_LastChat && pPlayer->m_LastChat+Server()->TickSpeed() > Server()->Tick())
 			return;
 		
-		pPlayer->m_Last_Chat = Server()->Tick();
+		pPlayer->m_LastChat = Server()->Tick();
 
 		// check for invalid chars
 		unsigned char *pMessage = (unsigned char *)pMsg->m_pMessage;
@@ -590,11 +600,11 @@ void CGameContext::OnMessage(int MsgID, CUnpacker *pUnpacker, int ClientID)
 	}
 	else if(MsgID == NETMSGTYPE_CL_CALLVOTE)
 	{
-		if(g_Config.m_SvSpamprotection && pPlayer->m_Last_VoteTry && pPlayer->m_Last_VoteTry+Server()->TickSpeed()*3 > Server()->Tick())
+		if(g_Config.m_SvSpamprotection && pPlayer->m_LastVoteTry && pPlayer->m_LastVoteTry+Server()->TickSpeed()*3 > Server()->Tick())
 			return;
 
 		int64 Now = Server()->Tick();
-		pPlayer->m_Last_VoteTry = Now;
+		pPlayer->m_LastVoteTry = Now;
 		if(pPlayer->GetTeam() == TEAM_SPECTATORS)
 		{
 			SendChatTarget(ClientID, "Spectators aren't allowed to start a vote.");
@@ -607,8 +617,8 @@ void CGameContext::OnMessage(int MsgID, CUnpacker *pUnpacker, int ClientID)
 			return;
 		}
 		
-		int Timeleft = pPlayer->m_Last_VoteCall + Server()->TickSpeed()*60 - Now;
-		if(pPlayer->m_Last_VoteCall && Timeleft > 0)
+		int Timeleft = pPlayer->m_LastVoteCall + Server()->TickSpeed()*60 - Now;
+		if(pPlayer->m_LastVoteCall && Timeleft > 0)
 		{
 			char aChatmsg[512] = {0};
 			str_format(aChatmsg, sizeof(aChatmsg), "You must wait %d seconds before making another vote", (Timeleft/Server()->TickSpeed())+1);
@@ -715,7 +725,7 @@ void CGameContext::OnMessage(int MsgID, CUnpacker *pUnpacker, int ClientID)
 			pPlayer->m_Vote = 1;
 			pPlayer->m_VotePos = m_VotePos = 1;
 			m_VoteCreator = ClientID;
-			pPlayer->m_Last_VoteCall = Now;
+			pPlayer->m_LastVoteCall = Now;
 		}
 	}
 	else if(MsgID == NETMSGTYPE_CL_VOTE)
@@ -738,7 +748,7 @@ void CGameContext::OnMessage(int MsgID, CUnpacker *pUnpacker, int ClientID)
 	{
 		CNetMsg_Cl_SetTeam *pMsg = (CNetMsg_Cl_SetTeam *)pRawMsg;
 		
-		if(pPlayer->GetTeam() == pMsg->m_Team || (g_Config.m_SvSpamprotection && pPlayer->m_Last_SetTeam && pPlayer->m_Last_SetTeam+Server()->TickSpeed()*3 > Server()->Tick()))
+		if(pPlayer->GetTeam() == pMsg->m_Team || (g_Config.m_SvSpamprotection && pPlayer->m_LastSetTeam && pPlayer->m_LastSetTeam+Server()->TickSpeed()*3 > Server()->Tick()))
 			return;
 
 		// Switch team on given client and kill/respawn him
@@ -746,7 +756,7 @@ void CGameContext::OnMessage(int MsgID, CUnpacker *pUnpacker, int ClientID)
 		{
 			if(m_pController->CanChangeTeam(pPlayer, pMsg->m_Team))
 			{
-				pPlayer->m_Last_SetTeam = Server()->Tick();
+				pPlayer->m_LastSetTeam = Server()->Tick();
 				if(pPlayer->GetTeam() == TEAM_SPECTATORS || pMsg->m_Team == TEAM_SPECTATORS)
 					m_VoteUpdate = true;
 				pPlayer->SetTeam(pMsg->m_Team);
@@ -762,14 +772,28 @@ void CGameContext::OnMessage(int MsgID, CUnpacker *pUnpacker, int ClientID)
 			SendBroadcast(aBuf, ClientID);
 		}
 	}
+	else if (MsgID == NETMSGTYPE_CL_SETSPECTATORMODE && !m_World.m_Paused)
+	{
+		CNetMsg_Cl_SetSpectatorMode *pMsg = (CNetMsg_Cl_SetSpectatorMode *)pRawMsg;
+		
+		if(pPlayer->GetTeam() != TEAM_SPECTATORS || pPlayer->m_SpectatorID == pMsg->m_SpectatorID || ClientID == pMsg->m_SpectatorID ||
+			(g_Config.m_SvSpamprotection && pPlayer->m_LastSetSpectatorMode && pPlayer->m_LastSetSpectatorMode+Server()->TickSpeed()*3 > Server()->Tick()))
+			return;
+
+		pPlayer->m_LastSetSpectatorMode = Server()->Tick();
+		if(pMsg->m_SpectatorID != SPEC_FREEVIEW && !m_apPlayers[pMsg->m_SpectatorID])
+			SendChatTarget(ClientID, "Invalid spectator id used");
+		else
+			pPlayer->m_SpectatorID = pMsg->m_SpectatorID;
+	}
 	else if (MsgID == NETMSGTYPE_CL_CHANGEINFO || MsgID == NETMSGTYPE_CL_STARTINFO)
 	{
 		CNetMsg_Cl_ChangeInfo *pMsg = (CNetMsg_Cl_ChangeInfo *)pRawMsg;
 		
-		if(g_Config.m_SvSpamprotection && pPlayer->m_Last_ChangeInfo && pPlayer->m_Last_ChangeInfo+Server()->TickSpeed()*5 > Server()->Tick())
+		if(g_Config.m_SvSpamprotection && pPlayer->m_LastChangeInfo && pPlayer->m_LastChangeInfo+Server()->TickSpeed()*5 > Server()->Tick())
 			return;
 			
-		pPlayer->m_Last_ChangeInfo = Server()->Tick();
+		pPlayer->m_LastChangeInfo = Server()->Tick();
 		
 		pPlayer->m_TeeInfos.m_UseCustomColor = pMsg->m_UseCustomColor;
 		pPlayer->m_TeeInfos.m_ColorBody = pMsg->m_ColorBody;
@@ -818,19 +842,19 @@ void CGameContext::OnMessage(int MsgID, CUnpacker *pUnpacker, int ClientID)
 	{
 		CNetMsg_Cl_Emoticon *pMsg = (CNetMsg_Cl_Emoticon *)pRawMsg;
 		
-		if(g_Config.m_SvSpamprotection && pPlayer->m_Last_Emote && pPlayer->m_Last_Emote+Server()->TickSpeed()*3 > Server()->Tick())
+		if(g_Config.m_SvSpamprotection && pPlayer->m_LastEmote && pPlayer->m_LastEmote+Server()->TickSpeed()*3 > Server()->Tick())
 			return;
 			
-		pPlayer->m_Last_Emote = Server()->Tick();
+		pPlayer->m_LastEmote = Server()->Tick();
 		
 		SendEmoticon(ClientID, pMsg->m_Emoticon);
 	}
 	else if (MsgID == NETMSGTYPE_CL_KILL && !m_World.m_Paused)
 	{
-		if(pPlayer->m_Last_Kill && pPlayer->m_Last_Kill+Server()->TickSpeed()*3 > Server()->Tick())
+		if(pPlayer->m_LastKill && pPlayer->m_LastKill+Server()->TickSpeed()*3 > Server()->Tick())
 			return;
 		
-		pPlayer->m_Last_Kill = Server()->Tick();
+		pPlayer->m_LastKill = Server()->Tick();
 		pPlayer->KillCharacter(WEAPON_SELF);
 		pPlayer->m_RespawnTick = Server()->Tick()+Server()->TickSpeed()*3;
 	}
diff --git a/src/game/server/player.cpp b/src/game/server/player.cpp
index 573d996d..7d2f4ad9 100644
--- a/src/game/server/player.cpp
+++ b/src/game/server/player.cpp
@@ -18,6 +18,7 @@ CPlayer::CPlayer(CGameContext *pGameServer, int ClientID, int Team)
 	Character = 0;
 	this->m_ClientID = ClientID;
 	m_Team = GameServer()->m_pController->ClampTeam(Team);
+	m_SpectatorID = SPEC_FREEVIEW;
 	m_LastActionTick = Server()->Tick();
 }
 
@@ -77,6 +78,23 @@ void CPlayer::Tick()
 		TryRespawn();
 }
 
+void CPlayer::PostTick()
+{
+	// update latency value
+	if(m_PlayerFlags&PLAYERFLAG_SCOREBOARD)
+	{
+		for(int i = 0; i < MAX_CLIENTS; ++i)
+		{
+			if(GameServer()->m_apPlayers[i] && GameServer()->m_apPlayers[i]->GetTeam() != TEAM_SPECTATORS)
+				m_aActLatency[i] = GameServer()->m_apPlayers[i]->m_Latency.m_Min;
+		}
+	}
+
+	// update view pos for spectators
+	if(m_Team == TEAM_SPECTATORS && m_SpectatorID != SPEC_FREEVIEW && GameServer()->m_apPlayers[m_SpectatorID])
+		m_ViewPos = GameServer()->m_apPlayers[m_SpectatorID]->m_ViewPos;
+}
+
 void CPlayer::Snap(int SnappingClient)
 {
 #ifdef CONF_DEBUG
@@ -99,12 +117,6 @@ void CPlayer::Snap(int SnappingClient)
 	if(!pPlayerInfo)
 		return;
 
-	// update latency value
-	if(SnappingClient != -1 && m_Team != -1 && GameServer()->m_apPlayers[SnappingClient]->m_PlayerFlags&PLAYERFLAG_SCOREBOARD)
-	{
-		GameServer()->m_apPlayers[SnappingClient]->m_aActLatency[m_ClientID] = m_Latency.m_Min;
-	}
-
 	pPlayerInfo->m_Latency = SnappingClient == -1 ? m_Latency.m_Min : GameServer()->m_apPlayers[SnappingClient]->m_aActLatency[m_ClientID];
 	pPlayerInfo->m_Local = 0;
 	pPlayerInfo->m_ClientID = m_ClientID;
@@ -112,7 +124,18 @@ void CPlayer::Snap(int SnappingClient)
 	pPlayerInfo->m_Team = m_Team;
 
 	if(m_ClientID == SnappingClient)
-		pPlayerInfo->m_Local = 1;	
+		pPlayerInfo->m_Local = 1;
+
+	if(m_ClientID == SnappingClient && m_Team == TEAM_SPECTATORS && m_SpectatorID != SPEC_FREEVIEW)
+	{
+		CNetObj_SpectatorInfo *pSpectatorInfo = static_cast<CNetObj_SpectatorInfo *>(Server()->SnapNewItem(NETOBJTYPE_SPECTATORINFO, m_ClientID, sizeof(CNetObj_SpectatorInfo)));
+		if(!pSpectatorInfo)
+			return;
+
+		pSpectatorInfo->m_SpectatorID = m_SpectatorID;
+		pSpectatorInfo->m_X = m_ViewPos.x;
+		pSpectatorInfo->m_Y = m_ViewPos.y;
+	}
 }
 
 void CPlayer::OnDisconnect()
@@ -146,7 +169,7 @@ void CPlayer::OnDirectInput(CNetObj_PlayerInput *NewInput)
 	if(!Character && m_Team != TEAM_SPECTATORS && (NewInput->m_Fire&1))
 		m_Spawning = true;
 	
-	if(!Character && m_Team == TEAM_SPECTATORS)
+	if(!Character && m_Team == TEAM_SPECTATORS && m_SpectatorID == SPEC_FREEVIEW)
 		m_ViewPos = vec2(NewInput->m_TargetX, NewInput->m_TargetY);
 
 	// check for activity
diff --git a/src/game/server/player.h b/src/game/server/player.h
index f4d82d24..c638fae8 100644
--- a/src/game/server/player.h
+++ b/src/game/server/player.h
@@ -25,6 +25,7 @@ public:
 	int GetCID() const { return m_ClientID; };
 	
 	void Tick();
+	void PostTick();
 	void Snap(int SnappingClient);
 
 	void OnDirectInput(CNetObj_PlayerInput *NewInput);
@@ -43,18 +44,22 @@ public:
 
 	// used for snapping to just update latency if the scoreboard is active
 	int m_aActLatency[MAX_CLIENTS];
+
+	// used for spectator mode
+	int m_SpectatorID;
 	
 	//
 	int m_Vote;
 	int m_VotePos;
 	//
-	int m_Last_VoteCall;
-	int m_Last_VoteTry;
-	int m_Last_Chat;
-	int m_Last_SetTeam;
-	int m_Last_ChangeInfo;
-	int m_Last_Emote;
-	int m_Last_Kill;
+	int m_LastVoteCall;
+	int m_LastVoteTry;
+	int m_LastChat;
+	int m_LastSetTeam;
+	int m_LastSetSpectatorMode;
+	int m_LastChangeInfo;
+	int m_LastEmote;
+	int m_LastKill;
 	
 	// TODO: clean this up
 	struct 
@@ -76,6 +81,17 @@ public:
 		int m_TargetX;
 		int m_TargetY;
 	} m_LatestActivity;
+
+	// network latency calculations	
+	struct
+	{
+		int m_Accum;
+		int m_AccumMin;
+		int m_AccumMax;
+		int m_Avg;
+		int m_Min;
+		int m_Max;	
+	} m_Latency;
 	
 private:
 	CCharacter *Character;
@@ -88,17 +104,6 @@ private:
 	bool m_Spawning;
 	int m_ClientID;
 	int m_Team;
-
-	// network latency calculations	
-	struct
-	{
-		int m_Accum;
-		int m_AccumMin;
-		int m_AccumMax;
-		int m_Avg;
-		int m_Min;
-		int m_Max;	
-	} m_Latency;
 };
 
 #endif