#include "nameplates.h"

#include <base/str.h>

#include <engine/graphics.h>
#include <engine/shared/config.h>
#include <engine/shared/protocol7.h>
#include <engine/textrender.h>

#include <generated/client_data.h>

#include <game/client/animstate.h>
#include <game/client/gameclient.h>
#include <game/client/prediction/entities/character.h>

#include <memory>
#include <vector>

enum class EHookStrongWeakState
{
	WEAK,
	NEUTRAL,
	STRONG
};

class CNamePlateData
{
public:
	bool m_Local; // TClient
	bool m_InGame;
	ColorRGBA m_Color;
	bool m_ShowName;
	char m_aName[std::max<size_t>(MAX_NAME_LENGTH, protocol7::MAX_NAME_ARRAY_SIZE)];
	bool m_ShowFriendMark;
	bool m_ShowClientId;
	int m_ClientId;
	float m_FontSizeClientId;
	bool m_ClientIdSeparateLine;
	float m_FontSize;
	bool m_ShowClan;
	char m_aClan[std::max<size_t>(MAX_CLAN_LENGTH, protocol7::MAX_CLAN_ARRAY_SIZE)];
	float m_FontSizeClan;
	bool m_ShowDirection;
	bool m_DirLeft;
	bool m_DirJump;
	bool m_DirRight;
	float m_FontSizeDirection;
	bool m_ShowHookStrongWeak;
	EHookStrongWeakState m_HookStrongWeakState;
	bool m_ShowHookStrongWeakId;
	int m_HookStrongWeakId;
	float m_FontSizeHookStrongWeak;
	bool m_CheckpointsEnabled; // CMClient
};

// Part Types

static constexpr float DEFAULT_PADDING = 5.0f;

class CNamePlatePart
{
protected:
	vec2 m_Size = vec2(0.0f, 0.0f);
	vec2 m_Padding = vec2(DEFAULT_PADDING, DEFAULT_PADDING);
	bool m_NewLine = false; // Whether this part is a new line (doesn't do anything else)
	bool m_Visible = true; // Whether this part is visible
	bool m_ShiftOnInvis = false; // Whether when not visible will still take up space
	CNamePlatePart(CGameClient &This) {}

public:
	virtual void Update(CGameClient &This, const CNamePlateData &Data) {}
	virtual void Reset(CGameClient &This) {}
	virtual void Render(CGameClient &This, vec2 Pos) const {}
	vec2 Size() const { return m_Size; }
	vec2 Padding() const { return m_Padding; }
	bool NewLine() const { return m_NewLine; }
	bool Visible() const { return m_Visible; }
	bool ShiftOnInvis() const { return m_ShiftOnInvis; }
	CNamePlatePart() = delete;
	virtual ~CNamePlatePart() = default;
};

using PartsVector = std::vector<std::unique_ptr<CNamePlatePart>>;

static constexpr ColorRGBA s_OutlineColor = ColorRGBA(0.0f, 0.0f, 0.0f, 0.5f);

class CNamePlatePartText : public CNamePlatePart
{
protected:
	STextContainerIndex m_TextContainerIndex;
	virtual bool UpdateNeeded(CGameClient &This, const CNamePlateData &Data) = 0;
	virtual void UpdateText(CGameClient &This, const CNamePlateData &Data) = 0;
	ColorRGBA m_Color = ColorRGBA(1.0f, 1.0f, 1.0f, 1.0f);
	CNamePlatePartText(CGameClient &This) :
		CNamePlatePart(This)
	{
		Reset(This);
	}

public:
	void Update(CGameClient &This, const CNamePlateData &Data) override
	{
		if(!UpdateNeeded(This, Data) && m_TextContainerIndex.Valid())
			return;

		// Set flags
		unsigned int Flags = ETextRenderFlags::TEXT_RENDER_FLAG_NO_FIRST_CHARACTER_X_BEARING | ETextRenderFlags::TEXT_RENDER_FLAG_NO_LAST_CHARACTER_ADVANCE;
		if(Data.m_InGame)
			Flags |= ETextRenderFlags::TEXT_RENDER_FLAG_NO_PIXEL_ALIGNMENT; // Prevent jittering from rounding
		This.TextRender()->SetRenderFlags(Flags);

		if(Data.m_InGame)
		{
			// Create text at standard zoom
			float ScreenX0, ScreenY0, ScreenX1, ScreenY1;
			This.Graphics()->GetScreen(&ScreenX0, &ScreenY0, &ScreenX1, &ScreenY1);
			This.Graphics()->MapScreenToInterface(This.m_Camera.m_Center.x, This.m_Camera.m_Center.y);
			This.TextRender()->DeleteTextContainer(m_TextContainerIndex);
			UpdateText(This, Data);
			This.Graphics()->MapScreen(ScreenX0, ScreenY0, ScreenX1, ScreenY1);
		}
		else
		{
			UpdateText(This, Data);
		}

		This.TextRender()->SetRenderFlags(0);

		if(!m_TextContainerIndex.Valid())
		{
			m_Visible = false;
			return;
		}

		const STextBoundingBox Container = This.TextRender()->GetBoundingBoxTextContainer(m_TextContainerIndex);
		m_Size = vec2(Container.m_W, Container.m_H);
	}
	void Reset(CGameClient &This) override
	{
		This.TextRender()->DeleteTextContainer(m_TextContainerIndex);
	}
	void Render(CGameClient &This, vec2 Pos) const override
	{
		if(!m_TextContainerIndex.Valid())
			return;

		ColorRGBA OutlineColor, Color;
		Color = m_Color;
		OutlineColor = s_OutlineColor.WithMultipliedAlpha(m_Color.a);
		This.TextRender()->RenderTextContainer(m_TextContainerIndex,
			Color, OutlineColor,
			Pos.x - Size().x / 2.0f, Pos.y - Size().y / 2.0f);
	}
};

class CNamePlatePartIcon : public CNamePlatePart
{
protected:
	IGraphics::CTextureHandle m_Texture;
	float m_Rotation = 0.0f;
	ColorRGBA m_Color = ColorRGBA(1.0f, 1.0f, 1.0f, 1.0f);
	CNamePlatePartIcon(CGameClient &This) :
		CNamePlatePart(This) {}

public:
	void Render(CGameClient &This, vec2 Pos) const override
	{
		IGraphics::CQuadItem QuadItem(Pos.x - Size().x / 2.0f, Pos.y - Size().y / 2.0f, Size().x, Size().y);
		This.Graphics()->TextureSet(m_Texture);
		This.Graphics()->QuadsBegin();
		This.Graphics()->SetColor(m_Color);
		This.Graphics()->QuadsSetRotation(m_Rotation);
		This.Graphics()->QuadsDrawTL(&QuadItem, 1);
		This.Graphics()->QuadsEnd();
		This.Graphics()->QuadsSetRotation(0.0f);
	}
};

class CNamePlatePartSprite : public CNamePlatePart
{
protected:
	IGraphics::CTextureHandle m_Texture;
	int m_Sprite = -1;
	int m_SpriteFlags = 0;
	float m_Rotation = 0.0f;
	ColorRGBA m_Color = ColorRGBA(1.0f, 1.0f, 1.0f, 1.0f);
	CNamePlatePartSprite(CGameClient &This) :
		CNamePlatePart(This) {}

public:
	void Render(CGameClient &This, vec2 Pos) const override
	{
		This.Graphics()->TextureSet(m_Texture);
		This.Graphics()->QuadsSetRotation(m_Rotation);
		This.Graphics()->QuadsBegin();
		This.Graphics()->SetColor(m_Color);
		This.Graphics()->SelectSprite(m_Sprite, m_SpriteFlags);
		This.Graphics()->DrawSprite(Pos.x, Pos.y, Size().x, Size().y);
		This.Graphics()->QuadsEnd();
		This.Graphics()->QuadsSetRotation(0.0f);
	}
};

// Part Definitions

class CNamePlatePartNewLine : public CNamePlatePart
{
public:
	CNamePlatePartNewLine(CGameClient &This) :
		CNamePlatePart(This)
	{
		m_NewLine = true;
	}
};

enum Direction
{
	DIRECTION_LEFT,
	DIRECTION_UP,
	DIRECTION_RIGHT
};

class CNamePlatePartDirection : public CNamePlatePartIcon
{
private:
	int m_Direction;
	bool m_IsBottom;

public:
	CNamePlatePartDirection(CGameClient &This, Direction Dir, bool IsBottom) :
		CNamePlatePartIcon(This)
	{
		m_Texture = g_pData->m_aImages[IMAGE_ARROW].m_Id;
		m_Direction = Dir;
		switch(m_Direction)
		{
		case DIRECTION_LEFT:
			m_Rotation = pi;
			break;
		case DIRECTION_UP:
			m_Rotation = pi / -2.0f;
			break;
		case DIRECTION_RIGHT:
			m_Rotation = 0.0f;
			break;
		}
		m_IsBottom = IsBottom;
	}
	void Update(CGameClient &This, const CNamePlateData &Data) override
	{
		if((g_Config.m_ClNameplateDirectionBottom && !m_IsBottom) || (!g_Config.m_ClNameplateDirectionBottom && m_IsBottom))
		{
			m_ShiftOnInvis = true;
			m_Visible = false;
			return;
		}
		if(!Data.m_ShowDirection)
		{
			m_ShiftOnInvis = false;
			m_Visible = false;
			return;
		}
		m_ShiftOnInvis = true; // Only shift (horizontally) the other parts if directions as a whole is visible
		m_Size = vec2(Data.m_FontSizeDirection, Data.m_FontSizeDirection);
		m_Padding.y = m_Size.y / 2.0f;
		switch(m_Direction)
		{
		case DIRECTION_LEFT:
			m_Visible = Data.m_DirLeft;
			break;
		case DIRECTION_UP:
			m_Visible = Data.m_DirJump;
			break;
		case DIRECTION_RIGHT:
			m_Visible = Data.m_DirRight;
			break;
		}
		m_Color.a = Data.m_Color.a;
	}
};

class CNamePlatePartClientId : public CNamePlatePartText
{
private:
	int m_ClientId = -1;
	static_assert(MAX_CLIENTS <= 999, "Make this buffer bigger");
	char m_aText[5] = "";
	float m_FontSize = -INFINITY;
	bool m_ClientIdSeparateLine = false;

protected:
	bool UpdateNeeded(CGameClient &This, const CNamePlateData &Data) override
	{
		m_Visible = Data.m_ShowClientId && (Data.m_ClientIdSeparateLine == m_ClientIdSeparateLine);
		if(!m_Visible)
			return false;
		m_Color = Data.m_Color;
		return m_FontSize != Data.m_FontSizeClientId || m_ClientId != Data.m_ClientId;
	}
	void UpdateText(CGameClient &This, const CNamePlateData &Data) override
	{
		m_FontSize = Data.m_FontSizeClientId;
		m_ClientId = Data.m_ClientId;
		if(m_ClientIdSeparateLine)
			str_format(m_aText, sizeof(m_aText), "%d", m_ClientId);
		else
			str_format(m_aText, sizeof(m_aText), "%d:", m_ClientId);
		CTextCursor Cursor;
		Cursor.m_FontSize = m_FontSize;
		This.TextRender()->CreateOrAppendTextContainer(m_TextContainerIndex, &Cursor, m_aText);
	}

public:
	CNamePlatePartClientId(CGameClient &This, bool ClientIdSeparateLine) :
		CNamePlatePartText(This)
	{
		m_ClientIdSeparateLine = ClientIdSeparateLine;
	}
};

class CNamePlatePartFriendMark : public CNamePlatePartText
{
private:
	float m_FontSize = -INFINITY;

protected:
	bool UpdateNeeded(CGameClient &This, const CNamePlateData &Data) override
	{
		m_Visible = Data.m_ShowFriendMark;
		if(!m_Visible)
			return false;
		//m_Color.a = Data.m_Color.a;
		// TClient + CMClient
		m_Color = ColorRGBA(1.0f, 0.0f, 0.0f, Data.m_Color.a);
		if(g_Config.m_TcWarList)
		{
			if(This.m_WarList.GetWarData(Data.m_ClientId).m_WarName)
				m_Color = This.m_WarList.GetNameplateColor(Data.m_ClientId).WithAlpha(Data.m_Color.a);
			else if(This.m_WarList.GetWarData(Data.m_ClientId).m_WarClan)
				m_Color = This.m_WarList.GetClanColor(Data.m_ClientId).WithAlpha(Data.m_Color.a);
		}
		return m_FontSize != Data.m_FontSize;
	}
	void UpdateText(CGameClient &This, const CNamePlateData &Data) override
	{
		m_FontSize = Data.m_FontSize;
		CTextCursor Cursor;
		This.TextRender()->SetFontPreset(EFontPreset::ICON_FONT);
		Cursor.m_FontSize = m_FontSize;
		This.TextRender()->CreateOrAppendTextContainer(m_TextContainerIndex, &Cursor, FontIcons::FONT_ICON_HEART);
		This.TextRender()->SetFontPreset(EFontPreset::DEFAULT_FONT);
	}

public:
	CNamePlatePartFriendMark(CGameClient &This) :
		CNamePlatePartText(This)
	{
		m_Color = ColorRGBA(1.0f, 0.0f, 0.0f);
	}
};

class CNamePlatePartName : public CNamePlatePartText
{
private:
	char m_aText[std::max<size_t>(MAX_NAME_LENGTH, protocol7::MAX_NAME_ARRAY_SIZE)] = "";
	float m_FontSize = -INFINITY;

protected:
	bool UpdateNeeded(CGameClient &This, const CNamePlateData &Data) override
	{
		m_Visible = Data.m_ShowName;
		if(!m_Visible)
			return false;
		m_Color = Data.m_Color;
		// TClient
		if(g_Config.m_TcWarList)
		{
			if(This.m_WarList.GetWarData(Data.m_ClientId).m_WarName)
				m_Color = This.m_WarList.GetNameplateColor(Data.m_ClientId).WithAlpha(Data.m_Color.a);
			else if(This.m_WarList.GetWarData(Data.m_ClientId).m_WarClan)
				m_Color = This.m_WarList.GetClanColor(Data.m_ClientId).WithAlpha(Data.m_Color.a);
		}
		return m_FontSize != Data.m_FontSize || str_comp(m_aText, Data.m_aName) != 0;
	}
	void UpdateText(CGameClient &This, const CNamePlateData &Data) override
	{
		m_FontSize = Data.m_FontSize;
		str_copy(m_aText, Data.m_aName, sizeof(m_aText));
		CTextCursor Cursor;
		Cursor.m_FontSize = m_FontSize;
		This.TextRender()->CreateOrAppendTextContainer(m_TextContainerIndex, &Cursor, m_aText);
	}

public:
	CNamePlatePartName(CGameClient &This) :
		CNamePlatePartText(This) {}
};

class CNamePlatePartClan : public CNamePlatePartText
{
private:
	char m_aText[std::max<size_t>(MAX_CLAN_LENGTH, protocol7::MAX_CLAN_ARRAY_SIZE)] = "";
	float m_FontSize = -INFINITY;

protected:
	bool UpdateNeeded(CGameClient &This, const CNamePlateData &Data) override
	{
		m_Visible = Data.m_ShowClan;
		if(!m_Visible && Data.m_aClan[0] != '\0')
			return false;
		m_Color = Data.m_Color;
		// TClient
		if(This.m_WarList.GetWarData(Data.m_ClientId).m_WarClan)
			m_Color = This.m_WarList.GetClanColor(Data.m_ClientId).WithAlpha(Data.m_Color.a);
		return m_FontSize != Data.m_FontSizeClan || str_comp(m_aText, Data.m_aClan) != 0;
	}
	void UpdateText(CGameClient &This, const CNamePlateData &Data) override
	{
		m_FontSize = Data.m_FontSizeClan;
		str_copy(m_aText, Data.m_aClan, sizeof(m_aText));
		CTextCursor Cursor;
		Cursor.m_FontSize = m_FontSize;
		This.TextRender()->CreateOrAppendTextContainer(m_TextContainerIndex, &Cursor, m_aText);
	}

public:
	CNamePlatePartClan(CGameClient &This) :
		CNamePlatePartText(This) {}
};

class CNamePlatePartHookStrongWeak : public CNamePlatePartSprite
{
protected:
	void Update(CGameClient &This, const CNamePlateData &Data) override
	{
		m_Visible = Data.m_ShowHookStrongWeak;
		if(!m_Visible)
			return;
		m_Size = vec2(Data.m_FontSizeHookStrongWeak + DEFAULT_PADDING, Data.m_FontSizeHookStrongWeak + DEFAULT_PADDING);
		switch(Data.m_HookStrongWeakState)
		{
		case EHookStrongWeakState::STRONG:
			m_Sprite = SPRITE_HOOK_STRONG;
			m_Color = color_cast<ColorRGBA>(ColorHSLA(6401973));
			break;
		case EHookStrongWeakState::NEUTRAL:
			m_Sprite = SPRITE_HOOK_ICON;
			m_Color = ColorRGBA(1.0f, 1.0f, 1.0f);
			break;
		case EHookStrongWeakState::WEAK:
			m_Sprite = SPRITE_HOOK_WEAK;
			m_Color = color_cast<ColorRGBA>(ColorHSLA(41131));
			break;
		}
		m_Color.a = Data.m_Color.a;
	}

public:
	CNamePlatePartHookStrongWeak(CGameClient &This) :
		CNamePlatePartSprite(This)
	{
		m_Texture = g_pData->m_aImages[IMAGE_STRONGWEAK].m_Id;
		m_Padding = vec2(0.0f, 0.0f);
	}
};

class CNamePlatePartHookStrongWeakId : public CNamePlatePartText
{
private:
	int m_StrongWeakId = -1;
	static_assert(MAX_CLIENTS <= 999, "Make this buffer bigger");
	char m_aText[4] = "";
	float m_FontSize = -INFINITY;

protected:
	bool UpdateNeeded(CGameClient &This, const CNamePlateData &Data) override
	{
		m_Visible = Data.m_ShowHookStrongWeakId;
		if(!m_Visible)
			return false;
		switch(Data.m_HookStrongWeakState)
		{
		case EHookStrongWeakState::STRONG:
			m_Color = color_cast<ColorRGBA>(ColorHSLA(6401973));
			break;
		case EHookStrongWeakState::NEUTRAL:
			m_Color = ColorRGBA(1.0f, 1.0f, 1.0f);
			break;
		case EHookStrongWeakState::WEAK:
			m_Color = color_cast<ColorRGBA>(ColorHSLA(41131));
			break;
		}
		m_Color.a = Data.m_Color.a;
		return m_FontSize != Data.m_FontSizeHookStrongWeak || m_StrongWeakId != Data.m_HookStrongWeakId;
	}
	void UpdateText(CGameClient &This, const CNamePlateData &Data) override
	{
		m_FontSize = Data.m_FontSizeHookStrongWeak;
		m_StrongWeakId = Data.m_HookStrongWeakId;
		str_format(m_aText, sizeof(m_aText), "%d", m_StrongWeakId);
		CTextCursor Cursor;
		Cursor.m_FontSize = m_FontSize;
		This.TextRender()->CreateOrAppendTextContainer(m_TextContainerIndex, &Cursor, m_aText);
	}

public:
	CNamePlatePartHookStrongWeakId(CGameClient &This) :
		CNamePlatePartText(This) {}
};

// ***** TClient Parts *****

class CNamePlatePartCountry : public CNamePlatePart
{
protected:
	static constexpr float FLAG_WIDTH = 128.0f;
	static constexpr float FLAG_HEIGHT = 64.0f;
	static constexpr float FLAG_RATIO = FLAG_HEIGHT / FLAG_WIDTH;
	const CCountryFlags::CCountryFlag *m_pCountryFlag = nullptr;
	float m_Alpha = 1.0f;

public:
	friend class CGameClient;
	void Update(CGameClient &This, const CNamePlateData &Data) override
	{
		m_Visible = true;
		if(g_Config.m_TcNameplateCountry == 0)
		{
			m_Visible = false;
			return;
		}
		if(Data.m_InGame)
		{
			// Check for us and dummy, Data.m_Local only does current char
			for(const auto Id : This.m_aLocalIds)
			{
				if(Id == Data.m_ClientId)
				{
					m_Visible = false;
					return;
				}
			}
			m_pCountryFlag = &This.m_CountryFlags.GetByCountryCode(This.m_aClients[Data.m_ClientId].m_Country);
		}
		else
		{
			if(Data.m_ClientId == 0)
				m_pCountryFlag = &This.m_CountryFlags.GetByCountryCode(g_Config.m_PlayerCountry);
			else
				m_pCountryFlag = &This.m_CountryFlags.GetByCountryCode(g_Config.m_ClDummyCountry);
		}
		// Do not show default flags
		if(m_pCountryFlag == &This.m_CountryFlags.GetByCountryCode(0))
		{
			m_Visible = false;
			return;
		}
		m_Alpha = Data.m_Color.a;
		m_Size = vec2(Data.m_FontSize / FLAG_RATIO, Data.m_FontSize);
	}
	void Render(CGameClient &This, vec2 Pos) const override
	{
		if(!m_pCountryFlag)
			return;
		This.m_CountryFlags.Render(*m_pCountryFlag, ColorRGBA(1.0f, 1.0f, 1.0f, m_Alpha),
			Pos.x - m_Size.x / 2.0f, Pos.y - m_Size.y / 2.0f,
			m_Size.x, m_Size.y);
	}
	CNamePlatePartCountry(CGameClient &This) :
		CNamePlatePart(This) {}
};

class CNamePlatePartPing : public CNamePlatePart
{
protected:
	float m_Radius = 7.0f;
	ColorRGBA m_Color;

public:
	friend class CGameClient;
	void Update(CGameClient &This, const CNamePlateData &Data) override
	{
		/*
			If in a real game,
				Don't show ping in demos
				Show other people's pings if in scoreboard
				Or if ping circle and name enabled
			If in preview
				Show ping if ping circle and name enabled
		*/
		m_Radius = (float)Data.m_FontSize * (7.0f / 28.0f);
		m_Size = vec2(m_Radius, m_Radius) * 2.0f;
		m_Visible = Data.m_InGame ? (
						    This.Client()->State() != IClient::STATE_DEMOPLAYBACK && ((Data.m_ShowName && g_Config.m_TcNameplatePingCircle > 0) ||
														     (This.m_Scoreboard.IsActive() && !This.m_Snap.m_apPlayerInfos[Data.m_ClientId]->m_Local))) :
					    (
						    (Data.m_ShowName && g_Config.m_TcNameplatePingCircle > 0));
		if(!m_Visible)
			return;
		int Ping = Data.m_InGame ? This.m_Snap.m_apPlayerInfos[Data.m_ClientId]->m_Latency : (1 + Data.m_ClientId) * 25;
		m_Color = color_cast<ColorRGBA>(ColorHSLA((float)(300 - std::clamp(Ping, 0, 300)) / 1000.0f, 1.0f, 0.5f, Data.m_Color.a));
	}
	void Render(CGameClient &This, vec2 Pos) const override
	{
		This.Graphics()->TextureClear();
		This.Graphics()->QuadsBegin();
		This.Graphics()->SetColor(ColorRGBA(0, 0, 0, 0.4f));
		This.Graphics()->DrawCircle(Pos.x, Pos.y, m_Radius * 1.25f, 24);
		This.Graphics()->SetColor(m_Color);
		This.Graphics()->DrawCircle(Pos.x, Pos.y, m_Radius, 24);
		This.Graphics()->QuadsEnd();
	}
	CNamePlatePartPing(CGameClient &This) :
		CNamePlatePart(This) {}
};

class CNamePlatePartSkin : public CNamePlatePartText
{
private:
	char m_aText[MAX_SKIN_LENGTH] = "";
	float m_FontSize = -INFINITY;

protected:
	bool UpdateNeeded(CGameClient &This, const CNamePlateData &Data) override
	{
		m_Visible = Data.m_InGame ? g_Config.m_TcNameplateSkins > (This.m_Snap.m_apPlayerInfos[Data.m_ClientId]->m_Local ? 1 : 0) : g_Config.m_TcNameplateSkins > 0;
		if(!m_Visible)
			return false;
		m_Color = Data.m_Color;
		const char *pSkin = Data.m_InGame ? This.m_aClients[Data.m_ClientId].m_aSkinName : (Data.m_ClientId == 0 ? g_Config.m_ClPlayerSkin : g_Config.m_ClDummySkin);
		return m_FontSize != Data.m_FontSizeClan || str_comp(m_aText, pSkin) != 0;
	}
	void UpdateText(CGameClient &This, const CNamePlateData &Data) override
	{
		m_FontSize = Data.m_FontSizeClan;
		const char *pSkin = Data.m_InGame ? This.m_aClients[Data.m_ClientId].m_aSkinName : (Data.m_ClientId == 0 ? g_Config.m_ClPlayerSkin : g_Config.m_ClDummySkin);
		str_copy(m_aText, pSkin, sizeof(m_aText));
		CTextCursor Cursor;
		Cursor.m_FontSize = m_FontSize;
		This.TextRender()->CreateOrAppendTextContainer(m_TextContainerIndex, &Cursor, m_aText);
	}

public:
	CNamePlatePartSkin(CGameClient &This) :
		CNamePlatePartText(This) {}
};

class CNamePlatePartReason : public CNamePlatePartText
{
private:
	char m_aText[MAX_WARLIST_REASON_LENGTH] = "";
	float m_FontSize = -INFINITY;

protected:
	bool UpdateNeeded(CGameClient &This, const CNamePlateData &Data) override
	{
		m_Visible = Data.m_InGame;
		if(!m_Visible)
			return false;
		const char *pReason = This.m_WarList.GetWarData(Data.m_ClientId).m_aReason;
		m_Visible = pReason[0] != '\0' && !This.m_Snap.m_apPlayerInfos[Data.m_ClientId]->m_Local;
		if(!m_Visible)
			return false;
		m_Color = Data.m_Color;
		m_Color.a *= 0.5f;
		//m_Color = ColorRGBA(0.7f, 0.7f, 0.7f, Data.m_Color.a);
		return m_FontSize != Data.m_FontSizeClan || str_comp(m_aText, pReason) != 0;
	}
	void UpdateText(CGameClient &This, const CNamePlateData &Data) override
	{
		m_FontSize = Data.m_FontSizeClan;
		const char *pReason = This.m_WarList.GetWarData(Data.m_ClientId).m_aReason;
		str_copy(m_aText, pReason, sizeof(m_aText));
		CTextCursor Cursor;
		Cursor.m_FontSize = m_FontSize;
		This.TextRender()->CreateOrAppendTextContainer(m_TextContainerIndex, &Cursor, m_aText);
	}

public:
	CNamePlatePartReason(CGameClient &This) :
		CNamePlatePartText(This) {}
};

class CNamePlatePartIgnoreMark : public CNamePlatePartText
{
private:
	float m_FontSize = -INFINITY;

protected:
	bool UpdateNeeded(CGameClient &This, const CNamePlateData &Data) override
	{
		m_Visible = (Data.m_InGame && Data.m_ShowName && This.Client()->State() != IClient::STATE_DEMOPLAYBACK && (This.m_aClients[Data.m_ClientId].m_Foe || This.m_aClients[Data.m_ClientId].m_ChatIgnore));
		if(!m_Visible)
			return false;
		m_Color = ColorRGBA(1.0f, 1.0f, 1.0f, Data.m_Color.a);
		return m_FontSize != Data.m_FontSize;
	}
	void UpdateText(CGameClient &This, const CNamePlateData &Data) override
	{
		m_FontSize = Data.m_FontSize;
		CTextCursor Cursor;
		This.TextRender()->SetFontPreset(EFontPreset::ICON_FONT);
		Cursor.m_FontSize = m_FontSize;
		This.TextRender()->CreateOrAppendTextContainer(m_TextContainerIndex, &Cursor, FontIcons::FONT_ICON_COMMENT_SLASH);
		This.TextRender()->SetFontPreset(EFontPreset::DEFAULT_FONT);
	}

public:
	CNamePlatePartIgnoreMark(CGameClient &This) :
		CNamePlatePartText(This) {}
};

// CMClient

class CNamePlatePartXSub : public CNamePlatePartText
{
private:
	float m_FontSize = -INFINITY;
	int m_X = -1;
	int m_LastTick = -1;
	bool m_CheckAligned = false;
	char m_Text[4];

protected:
	bool UpdateNeeded(CGameClient &This, const CNamePlateData &Data) override
	{
		if(!This.m_GameInfo.m_HudDDRace || g_Config.m_ClNameplateXSub == 0)
		{
			m_Visible = false;
			return false;
		}

		m_Color = color_cast<ColorRGBA>(ColorHSLA((Data.m_InGame && This.m_Snap.m_apPlayerInfos[Data.m_ClientId]->m_Local) ? g_Config.m_ClNameplateXSubColorOwn : g_Config.m_ClNameplateXSubColorOther, true)).WithMultipliedAlpha(Data.m_Color.a);
		if(m_LastTick < 0 || m_X != (Data.m_InGame ? This.m_aClients[Data.m_ClientId].m_RenderCur.m_X : 17))
		{
			m_X = Data.m_InGame ? This.m_aClients[Data.m_ClientId].m_RenderCur.m_X : 17;
			m_LastTick = This.Client()->GameTick(0), m_CheckAligned = m_LastTick < 0;
			m_Visible = g_Config.m_ClNameplateXSub != 2;
			return m_Visible;
		}
		else if(This.Client()->GameTick(0) - m_LastTick > g_Config.m_ClNameplateXSubSymbolDelay && !m_CheckAligned)
		{
			m_CheckAligned = true;
			m_Visible = g_Config.m_ClNameplateXSub != 2 || (g_Config.m_ClNameplateXSubSymbolWalled && ((m_X & 31) == 14 || (m_X & 31) == 17)) || (g_Config.m_ClNameplateXSubSymbolCentered && (m_X & 31) == 16);
			return m_Visible;
		}
		//if (m_LastTick < 0 || m_X != This.m_aClients[Data.m_ClientId].m_RenderCur.m_X || (This.Client()->GameTick(0)-m_LastTick > 5 && !m_CheckAligned)) return true;
		return m_FontSize != Data.m_FontSize;
	}
	void UpdateText(CGameClient &This, const CNamePlateData &Data) override
	{
		float fX = ((float)m_X) / 32.0f;
		snprintf(m_Text, 4, ".%02i", (int)round((fX - floor(fX)) * 100.0f - 0.01));
		if(m_CheckAligned)
		{
			if(g_Config.m_ClNameplateXSubSymbolWalled && (m_X & 31) == 14)
				snprintf(m_Text, 4, "⇤");
			if(g_Config.m_ClNameplateXSubSymbolCentered && (m_X & 31) == 16)
				snprintf(m_Text, 4, "|");
			if(g_Config.m_ClNameplateXSubSymbolWalled && (m_X & 31) == 17)
				snprintf(m_Text, 4, "⇥");
		}
		m_FontSize = Data.m_FontSize;
		CTextCursor Cursor;
		Cursor.m_FontSize = m_FontSize;
		This.TextRender()->CreateOrAppendTextContainer(m_TextContainerIndex, &Cursor, m_Text);
	}

public:
	CNamePlatePartXSub(CGameClient &This) :
		CNamePlatePartText(This) {}
};

class CNamePlatePartCheckpoint : public CNamePlatePartText
{
private:
	float m_FontSize = -INFINITY;
	int m_Checkpoint = -1;
	char m_Text[4];

protected:
	bool UpdateNeeded(CGameClient &This, const CNamePlateData &Data) override
	{
		if(!This.m_GameInfo.m_HudDDRace)
		{
			m_Visible = false;
			return false;
		}
		m_Visible = (g_Config.m_ClNameplateCheckpoint == 2 || (g_Config.m_ClNameplateCheckpoint == 1 && Data.m_CheckpointsEnabled)) && This.m_aClients[Data.m_ClientId].m_Predicted.m_TeleCheckpoint != -1 && (g_Config.m_ClNameplateCheckpointShowZero || This.m_aClients[Data.m_ClientId].m_Predicted.m_TeleCheckpoint != 0);
		if(!m_Visible)
			return false;
		m_Color = color_cast<ColorRGBA>(ColorHSLA((Data.m_InGame && This.m_Snap.m_apPlayerInfos[Data.m_ClientId]->m_Local) ? g_Config.m_ClNameplateCheckpointColorOwn : g_Config.m_ClNameplateCheckpointColorOther, true)).WithMultipliedAlpha(Data.m_Color.a * (g_Config.m_ClNameplateCheckpointShowZero == 2 && ((Data.m_InGame ? This.m_aClients[Data.m_ClientId].m_Predicted.m_TeleCheckpoint : 7) == 0) ? 0.2f : 1.0f));
		if(m_Checkpoint == -1 || m_Checkpoint != (Data.m_InGame ? This.m_aClients[Data.m_ClientId].m_Predicted.m_TeleCheckpoint : 7))
			return true;
		return m_FontSize != Data.m_FontSize;
	}
	void UpdateText(CGameClient &This, const CNamePlateData &Data) override
	{
		if(m_Checkpoint == -1 || m_Checkpoint != (Data.m_InGame ? This.m_aClients[Data.m_ClientId].m_Predicted.m_TeleCheckpoint : 7))
		{
			m_Checkpoint = Data.m_InGame ? This.m_aClients[Data.m_ClientId].m_Predicted.m_TeleCheckpoint : 7;
			snprintf(m_Text, 4, "%i", m_Checkpoint);
		}
		m_FontSize = Data.m_FontSize;
		CTextCursor Cursor;
		Cursor.m_FontSize = m_FontSize;
		This.TextRender()->CreateOrAppendTextContainer(m_TextContainerIndex, &Cursor, m_Text);
	}

public:
	CNamePlatePartCheckpoint(CGameClient &This) :
		CNamePlatePartText(This) {}
};

enum PlayerState
{
	PLAYERSTATE_WEAPON_HAMMER,
	PLAYERSTATE_WEAPON_FIRST = PLAYERSTATE_WEAPON_HAMMER,
	PLAYERSTATE_WEAPON_GUN,
	PLAYERSTATE_WEAPON_SHOTGUN,
	PLAYERSTATE_WEAPON_GRENADE,
	PLAYERSTATE_WEAPON_LASER,
	PLAYERSTATE_WEAPON_LAST = PLAYERSTATE_WEAPON_LASER,
	PLAYERSTATE_CAPABILITY_ENDLESSJUMP,
	PLAYERSTATE_CAPABILITY_FIRST = PLAYERSTATE_CAPABILITY_ENDLESSJUMP,
	PLAYERSTATE_CAPABILITY_ENDLESSHOOK,
	PLAYERSTATE_CAPABILITY_JETPACK,
	PLAYERSTATE_CAPABILITY_TELEPORTGUN,
	PLAYERSTATE_CAPABILITY_TELEPORTGRENADE,
	PLAYERSTATE_CAPABILITY_TELEPORTLASER,
	PLAYERSTATE_PROHIBITEDCAPABILITY_COLLISIONDISABLED,
	PLAYERSTATE_PROHIBITEDCAPABILITY_HOOKHITDISABLED,
	PLAYERSTATE_PROHIBITEDCAPABILITY_HAMMERHITDISABLED,
	PLAYERSTATE_PROHIBITEDCAPABILITY_GUNHITDISABLED,
	PLAYERSTATE_PROHIBITEDCAPABILITY_SHOTGUNHITDISABLED,
	PLAYERSTATE_PROHIBITEDCAPABILITY_GRENADEHITDISABLED,
	PLAYERSTATE_PROHIBITEDCAPABILITY_LASERHITDISABLED,
	PLAYERSTATE_CAPABILITY_LAST = PLAYERSTATE_PROHIBITEDCAPABILITY_LASERHITDISABLED,
};
class CNamePlatePartPlayerState : public CNamePlatePartIcon
{
private:
	PlayerState m_PlayerState;

protected:
	void Update(CGameClient &This, const CNamePlateData &Data) override
	{
		if(!This.m_GameInfo.m_HudDDRace)
		{
			m_Visible = false;
			return;
		}
		if(!Data.m_InGame)
		{
			bool HasWeapons = g_Config.m_ClNameplateWeapons;
			if(!HasWeapons && m_PlayerState >= PLAYERSTATE_WEAPON_FIRST && m_PlayerState <= PLAYERSTATE_WEAPON_LAST)
			{
				m_Visible = false;
				return;
			}
			if(!(g_Config.m_ClNameplateCapabilities & 1) && m_PlayerState >= PLAYERSTATE_CAPABILITY_FIRST && m_PlayerState <= PLAYERSTATE_CAPABILITY_LAST)
			{
				m_Visible = false;
				return;
			}
			m_Size = vec2(24.0f, 24.0f);
			m_Rotation = 0.0f;
			m_Color = ColorRGBA(1.0f, 1.0f, 1.0f, 0.4f);
			switch(m_PlayerState)
			{
			case PLAYERSTATE_WEAPON_HAMMER:
				m_Texture = This.m_GameSkin.m_aSpritePickupWeapons[WEAPON_HAMMER];
				m_Rotation = pi * 7.0f / 4.0f;
				m_Size.y /= 128.0f / 96.0f;
				m_Visible = true;
				m_Color.a = 1.0f;
				break;
			case PLAYERSTATE_WEAPON_GUN:
				m_Texture = This.m_HudSkin.m_SpriteHudJetpack;
				m_Visible = true;
				break;
			case PLAYERSTATE_CAPABILITY_ENDLESSJUMP:
				m_Texture = This.m_HudSkin.m_SpriteHudEndlessJump;
				m_Visible = true;
				break;
			case PLAYERSTATE_CAPABILITY_ENDLESSHOOK:
				m_Texture = This.m_HudSkin.m_SpriteHudEndlessHook;
				m_Visible = true;
				break;
			case PLAYERSTATE_CAPABILITY_JETPACK:
				m_Texture = This.m_HudSkin.m_SpriteHudJetpack;
				m_Visible = !HasWeapons;
				break;
			default:
				m_Visible = false;
				break;
			}
			return;
		}
		bool HasWeapons = g_Config.m_ClNameplateWeapons & (1 << (This.m_Snap.m_apPlayerInfos[Data.m_ClientId]->m_Local ? 1 : 0));
		if(!HasWeapons && m_PlayerState >= PLAYERSTATE_WEAPON_FIRST && m_PlayerState <= PLAYERSTATE_WEAPON_LAST)
		{
			m_Visible = false;
			return;
		}
		if(!(g_Config.m_ClNameplateCapabilities & (1 << (This.m_Snap.m_apPlayerInfos[Data.m_ClientId]->m_Local ? 1 : 0))) && m_PlayerState >= PLAYERSTATE_CAPABILITY_FIRST && m_PlayerState <= PLAYERSTATE_CAPABILITY_LAST)
		{
			m_Visible = false;
			return;
		}
		/*if (Data.m_ClientId == (This.m_Snap.m_SpecInfo.m_Active ? This.m_Snap.m_SpecInfo.m_SpectatorId : This.m_Snap.m_LocalClientId))
		{
		  m_Visible = false;
		  return;
		}*/
		CCharacterCore *pCharacter = &This.m_aClients[Data.m_ClientId].m_Predicted;
		CNetObj_Character *pPlayer = &This.m_aClients[Data.m_ClientId].m_RenderCur;
		m_Size = vec2(24.0f, 24.0f);
		m_Rotation = 0.0f;
		m_Color = ColorRGBA(1.0f, 1.0f, 1.0f, 0.4f);
		switch(m_PlayerState)
		{
		case PLAYERSTATE_WEAPON_HAMMER:
			if(pCharacter->m_HammerHitDisabled)
				m_Texture = This.m_HudSkin.m_SpriteHudHammerHitDisabled;
			else
			{
				m_Texture = This.m_GameSkin.m_aSpritePickupWeapons[WEAPON_HAMMER];
				m_Rotation = pi * 7.0f / 4.0f;
				m_Size.y /= 128.0f / 96.0f;
			}
			m_Visible = pCharacter->m_aWeapons[WEAPON_HAMMER].m_Got;
			if(pPlayer->m_Weapon == WEAPON_HAMMER)
				m_Color.a = 1.0f;
			break;
		case PLAYERSTATE_WEAPON_GUN:
			if(pCharacter->m_Jetpack)
				m_Texture = This.m_HudSkin.m_SpriteHudJetpack;
			else if(pCharacter->m_GrenadeHitDisabled && pCharacter->m_HasTelegunGun)
				m_Texture = This.m_HudSkin.m_SpriteHudGunHitDisabled;
			else if(pCharacter->m_HasTelegunGun)
				m_Texture = This.m_HudSkin.m_SpriteHudTeleportGun;
			else
			{
				m_Texture = This.m_GameSkin.m_aSpritePickupWeapons[WEAPON_GUN];
				m_Rotation = pi * 7.0f / 4.0f;
				m_Size.y /= 128.0f / 64.0f;
			}
			m_Visible = pCharacter->m_aWeapons[WEAPON_GUN].m_Got;
			if(pPlayer->m_Weapon == WEAPON_GUN)
				m_Color.a = 1.0f;
			break;
		case PLAYERSTATE_WEAPON_SHOTGUN:
			if(pCharacter->m_ShotgunHitDisabled)
				m_Texture = This.m_HudSkin.m_SpriteHudShotgunHitDisabled;
			else
			{
				m_Texture = This.m_GameSkin.m_aSpritePickupWeapons[WEAPON_SHOTGUN];
				m_Rotation = pi * 7.0f / 4.0f;
				m_Size.y /= 256.0f / 64.0f;
			}
			m_Visible = pCharacter->m_aWeapons[WEAPON_SHOTGUN].m_Got;
			if(pPlayer->m_Weapon == WEAPON_SHOTGUN)
				m_Color.a = 1.0f;
			break;
		case PLAYERSTATE_WEAPON_GRENADE:
			if(pCharacter->m_GrenadeHitDisabled)
				m_Texture = This.m_HudSkin.m_SpriteHudGrenadeHitDisabled;
			else if(pCharacter->m_HasTelegunGrenade)
				m_Texture = This.m_HudSkin.m_SpriteHudTeleportGrenade;
			else
			{
				m_Texture = This.m_GameSkin.m_aSpritePickupWeapons[WEAPON_GRENADE];
				m_Rotation = pi * 7.0f / 4.0f;
				m_Size.y /= 224.0f / 64.0f;
			}
			m_Visible = pCharacter->m_aWeapons[WEAPON_GRENADE].m_Got;
			if(pPlayer->m_Weapon == WEAPON_GRENADE)
				m_Color.a = 1.0f;
			break;
		case PLAYERSTATE_WEAPON_LASER:
			if(pCharacter->m_HasTelegunLaser)
				m_Texture = This.m_HudSkin.m_SpriteHudTeleportLaser;
			else if(pCharacter->m_LaserHitDisabled)
				m_Texture = This.m_HudSkin.m_SpriteHudLaserHitDisabled;
			else
			{
				m_Texture = This.m_GameSkin.m_aSpritePickupWeapons[WEAPON_LASER];
				m_Rotation = pi * 7.0f / 4.0f;
				m_Size.y /= 224.0f / 96.0f;
			}
			m_Visible = pCharacter->m_aWeapons[WEAPON_LASER].m_Got;
			if(pPlayer->m_Weapon == WEAPON_LASER)
				m_Color.a = 1.0f;
			break;
		case PLAYERSTATE_CAPABILITY_ENDLESSJUMP:
			m_Texture = This.m_HudSkin.m_SpriteHudEndlessJump;
			m_Visible = pCharacter->m_EndlessJump;
			break;
		case PLAYERSTATE_CAPABILITY_ENDLESSHOOK:
			m_Texture = This.m_HudSkin.m_SpriteHudEndlessHook;
			m_Visible = pCharacter->m_EndlessHook;
			break;
		case PLAYERSTATE_CAPABILITY_JETPACK:
			m_Texture = This.m_HudSkin.m_SpriteHudJetpack;
			m_Visible = pCharacter->m_Jetpack && (!HasWeapons || !pCharacter->m_aWeapons[WEAPON_GUN].m_Got);
			break;
		case PLAYERSTATE_CAPABILITY_TELEPORTGUN:
			m_Texture = This.m_HudSkin.m_SpriteHudTeleportGun;
			m_Visible = pCharacter->m_HasTelegunGun && ((!HasWeapons || !pCharacter->m_aWeapons[WEAPON_GUN].m_Got) || pCharacter->m_Jetpack || (pCharacter->m_GrenadeHitDisabled && pCharacter->m_HasTelegunGun && pCharacter->m_aWeapons[WEAPON_GUN].m_Got));
			break;
		case PLAYERSTATE_CAPABILITY_TELEPORTGRENADE:
			m_Texture = This.m_HudSkin.m_SpriteHudTeleportGrenade;
			m_Visible = pCharacter->m_HasTelegunGrenade && ((!HasWeapons || !pCharacter->m_aWeapons[WEAPON_GRENADE].m_Got) || pCharacter->m_GrenadeHitDisabled);
			break;
		case PLAYERSTATE_CAPABILITY_TELEPORTLASER:
			m_Texture = This.m_HudSkin.m_SpriteHudTeleportLaser;
			m_Visible = pCharacter->m_HasTelegunLaser && (!HasWeapons || !pCharacter->m_aWeapons[WEAPON_LASER].m_Got);
			break;
		case PLAYERSTATE_PROHIBITEDCAPABILITY_COLLISIONDISABLED:
			m_Texture = This.m_HudSkin.m_SpriteHudCollisionDisabled;
			m_Visible = pCharacter->m_CollisionDisabled;
			break;
		case PLAYERSTATE_PROHIBITEDCAPABILITY_HOOKHITDISABLED:
			m_Texture = This.m_HudSkin.m_SpriteHudHookHitDisabled;
			m_Visible = pCharacter->m_HookHitDisabled;
			break;
		case PLAYERSTATE_PROHIBITEDCAPABILITY_HAMMERHITDISABLED:
			m_Texture = This.m_HudSkin.m_SpriteHudHammerHitDisabled;
			m_Visible = pCharacter->m_HammerHitDisabled && (!HasWeapons || !pCharacter->m_aWeapons[WEAPON_HAMMER].m_Got);
			break;
		case PLAYERSTATE_PROHIBITEDCAPABILITY_GUNHITDISABLED:
			m_Texture = This.m_HudSkin.m_SpriteHudGunHitDisabled;
			m_Visible = (pCharacter->m_GrenadeHitDisabled && pCharacter->m_HasTelegunGun) && ((!HasWeapons || !pCharacter->m_aWeapons[WEAPON_GUN].m_Got) || pCharacter->m_Jetpack);
			break;
		case PLAYERSTATE_PROHIBITEDCAPABILITY_SHOTGUNHITDISABLED:
			m_Texture = This.m_HudSkin.m_SpriteHudShotgunHitDisabled;
			m_Visible = pCharacter->m_ShotgunHitDisabled && (!HasWeapons || !pCharacter->m_aWeapons[WEAPON_SHOTGUN].m_Got);
			break;
		case PLAYERSTATE_PROHIBITEDCAPABILITY_GRENADEHITDISABLED:
			m_Texture = This.m_HudSkin.m_SpriteHudGrenadeHitDisabled;
			m_Visible = pCharacter->m_GrenadeHitDisabled && (!HasWeapons || !pCharacter->m_aWeapons[WEAPON_GRENADE].m_Got);
			break;
		case PLAYERSTATE_PROHIBITEDCAPABILITY_LASERHITDISABLED:
			m_Texture = This.m_HudSkin.m_SpriteHudLaserHitDisabled;
			m_Visible = pCharacter->m_LaserHitDisabled && ((!HasWeapons || !pCharacter->m_aWeapons[WEAPON_LASER].m_Got) || pCharacter->m_HasTelegunLaser);
			break;
		default:
			m_Visible = false;
		}
		m_Color.a *= Data.m_Color.a;
	}

public:
	CNamePlatePartPlayerState(CGameClient &This, PlayerState State) :
		CNamePlatePartIcon(This)
	{
		m_PlayerState = State;
	}
};

// ***** Name Plates *****

class CNamePlate
{
private:
	bool m_Inited = false;
	bool m_InGame = false;
	PartsVector m_vpPartsTop, m_vpPartsBottom;
	void RenderLine(CGameClient &This,
		vec2 Pos, vec2 Size,
		PartsVector::iterator Start, PartsVector::iterator End)
	{
		Pos.x -= Size.x / 2.0f;
		for(auto PartIt = Start; PartIt != End; ++PartIt)
		{
			const CNamePlatePart &Part = **PartIt;
			if(Part.Visible())
			{
				Part.Render(This, vec2(
							  Pos.x + (Part.Padding().x + Part.Size().x) / 2.0f,
							  Pos.y - std::max(Size.y, Part.Padding().y + Part.Size().y) / 2.0f));
			}
			if(Part.Visible() || Part.ShiftOnInvis())
				Pos.x += Part.Size().x + Part.Padding().x;
		}
	}
	template<typename PartType, typename... ArgsType>
	void AddPartTop(CGameClient &This, ArgsType &&...Args)
	{
		m_vpPartsTop.push_back(std::make_unique<PartType>(This, std::forward<ArgsType>(Args)...));
	}
	template<typename PartType, typename... ArgsType>
	void AddPartBottom(CGameClient &This, ArgsType &&...Args)
	{
		m_vpPartsBottom.push_back(std::make_unique<PartType>(This, std::forward<ArgsType>(Args)...));
	}
	void Init(CGameClient &This)
	{
		if(m_Inited)
			return;
		m_Inited = true;

		AddPartTop<CNamePlatePartCountry>(This); // TClient
		AddPartTop<CNamePlatePartPing>(This); // TClient
		AddPartTop<CNamePlatePartIgnoreMark>(This); // TClient
		AddPartTop<CNamePlatePartFriendMark>(This);
		AddPartTop<CNamePlatePartClientId>(This, false);
		AddPartTop<CNamePlatePartName>(This);
		AddPartTop<CNamePlatePartNewLine>(This);

		AddPartTop<CNamePlatePartClan>(This);
		AddPartTop<CNamePlatePartNewLine>(This);

		AddPartTop<CNamePlatePartReason>(This); // TClient
		AddPartTop<CNamePlatePartNewLine>(This); // TClient
		AddPartTop<CNamePlatePartSkin>(This); // TClient
		AddPartTop<CNamePlatePartNewLine>(This); // TClient

		AddPartTop<CNamePlatePartClientId>(This, true);
		AddPartTop<CNamePlatePartNewLine>(This);

		AddPartTop<CNamePlatePartCheckpoint>(This); // CMClient
		AddPartTop<CNamePlatePartXSub>(This); // CMClient
		AddPartTop<CNamePlatePartHookStrongWeak>(This);
		AddPartTop<CNamePlatePartHookStrongWeakId>(This);
		AddPartTop<CNamePlatePartNewLine>(This);

		AddPartTop<CNamePlatePartPlayerState>(This, PLAYERSTATE_CAPABILITY_ENDLESSJUMP); // CMClient
		AddPartTop<CNamePlatePartPlayerState>(This, PLAYERSTATE_CAPABILITY_ENDLESSHOOK); // CMClient
		AddPartTop<CNamePlatePartPlayerState>(This, PLAYERSTATE_CAPABILITY_JETPACK); // CMClient
		AddPartTop<CNamePlatePartPlayerState>(This, PLAYERSTATE_CAPABILITY_TELEPORTGUN); // CMClient
		AddPartTop<CNamePlatePartPlayerState>(This, PLAYERSTATE_CAPABILITY_TELEPORTGRENADE); // CMClient
		AddPartTop<CNamePlatePartPlayerState>(This, PLAYERSTATE_CAPABILITY_TELEPORTLASER); // CMClient
		AddPartTop<CNamePlatePartPlayerState>(This, PLAYERSTATE_PROHIBITEDCAPABILITY_COLLISIONDISABLED); // CMClient
		AddPartTop<CNamePlatePartPlayerState>(This, PLAYERSTATE_PROHIBITEDCAPABILITY_HOOKHITDISABLED); // CMClient
		AddPartTop<CNamePlatePartPlayerState>(This, PLAYERSTATE_PROHIBITEDCAPABILITY_HAMMERHITDISABLED); // CMClient
		AddPartTop<CNamePlatePartPlayerState>(This, PLAYERSTATE_PROHIBITEDCAPABILITY_GUNHITDISABLED); // CMClient
		AddPartTop<CNamePlatePartPlayerState>(This, PLAYERSTATE_PROHIBITEDCAPABILITY_SHOTGUNHITDISABLED); // CMClient
		AddPartTop<CNamePlatePartPlayerState>(This, PLAYERSTATE_PROHIBITEDCAPABILITY_GRENADEHITDISABLED); // CMClient
		AddPartTop<CNamePlatePartPlayerState>(This, PLAYERSTATE_PROHIBITEDCAPABILITY_LASERHITDISABLED); // CMClient
		AddPartTop<CNamePlatePartNewLine>(This);
		AddPartTop<CNamePlatePartPlayerState>(This, PLAYERSTATE_WEAPON_HAMMER); // CMClient
		AddPartTop<CNamePlatePartPlayerState>(This, PLAYERSTATE_WEAPON_GUN); // CMClient
		AddPartTop<CNamePlatePartPlayerState>(This, PLAYERSTATE_WEAPON_SHOTGUN); // CMClient
		AddPartTop<CNamePlatePartPlayerState>(This, PLAYERSTATE_WEAPON_GRENADE); // CMClient
		AddPartTop<CNamePlatePartPlayerState>(This, PLAYERSTATE_WEAPON_LASER); // CMClient
		AddPartTop<CNamePlatePartNewLine>(This);

		AddPartTop<CNamePlatePartDirection>(This, DIRECTION_LEFT, false);
		AddPartTop<CNamePlatePartDirection>(This, DIRECTION_UP, false);
		AddPartTop<CNamePlatePartDirection>(This, DIRECTION_RIGHT, false);

		AddPartBottom<CNamePlatePartDirection>(This, DIRECTION_LEFT, true);
		AddPartBottom<CNamePlatePartDirection>(This, DIRECTION_UP, true);
		AddPartBottom<CNamePlatePartDirection>(This, DIRECTION_RIGHT, true);
	}

public:
	CNamePlate() = default;
	CNamePlate(CGameClient &This, const CNamePlateData &Data)
	{
		// Convenience constructor
		Update(This, Data);
	}
	void Reset(CGameClient &This)
	{
		for(auto &Part : m_vpPartsTop)
			Part->Reset(This);
		for(auto &Part : m_vpPartsBottom)
			Part->Reset(This);
	}
	void Update(CGameClient &This, const CNamePlateData &Data)
	{
		Init(This);
		m_InGame = Data.m_InGame;
		for(auto &Part : m_vpPartsTop)
			Part->Update(This, Data);
		for(auto &Part : m_vpPartsBottom)
			Part->Update(This, Data);
	}
	void RenderTop(CGameClient &This, const vec2 &PositionBottomMiddle)
	{
		dbg_assert(m_Inited, "Tried to render uninited nameplate");
		vec2 Position = PositionBottomMiddle;
		// X: Total width including padding of line, Y: Max height of line parts
		vec2 LineSize = vec2(0.0f, 0.0f);
		bool Empty = true;
		auto Start = m_vpPartsTop.begin();
		for(auto PartIt = m_vpPartsTop.begin(); PartIt != m_vpPartsTop.end(); ++PartIt)
		{
			CNamePlatePart &Part = **PartIt;
			if(Part.NewLine())
			{
				if(!Empty)
				{
					RenderLine(This, Position, LineSize, Start, std::next(PartIt));
					Position.y -= LineSize.y;
				}
				Start = std::next(PartIt);
				LineSize = vec2(0.0f, 0.0f);
			}
			else if(Part.Visible() || Part.ShiftOnInvis())
			{
				Empty = false;
				LineSize.x += Part.Size().x + Part.Padding().x;
				LineSize.y = std::max(LineSize.y, Part.Size().y + Part.Padding().y);
			}
		}
		RenderLine(This, Position, LineSize, Start, m_vpPartsTop.end());
		This.Graphics()->SetColor(1.0f, 1.0f, 1.0f, 1.0f);
	}
	void RenderBottom(CGameClient &This, const vec2 &PositionTopMiddle)
	{
		dbg_assert(m_Inited, "Tried to render uninited nameplate");
		vec2 Position = PositionTopMiddle;
		// X: Total width including padding of line, Y: Max height of line parts
		vec2 LineSize = vec2(0.0f, 0.0f);
		bool Empty = true;
		auto Start = m_vpPartsBottom.begin();
		for(auto PartIt = m_vpPartsBottom.begin(); PartIt != m_vpPartsBottom.end(); ++PartIt)
		{
			CNamePlatePart &Part = **PartIt;
			if(Part.NewLine())
			{
				if(!Empty)
				{
					RenderLine(This, Position, LineSize, Start, std::next(PartIt));
					Position.y += LineSize.y;
				}
				Start = std::next(PartIt);
				LineSize = vec2(0.0f, 0.0f);
			}
			else if(Part.Visible() || Part.ShiftOnInvis())
			{
				Empty = false;
				LineSize.x += Part.Size().x + Part.Padding().x;
				LineSize.y = std::max(LineSize.y, Part.Size().y + Part.Padding().y);
			}
		}
		RenderLine(This, Position, LineSize, Start, m_vpPartsBottom.end());
		Position.y += LineSize.y;
		This.Graphics()->SetColor(1.0f, 1.0f, 1.0f, 1.0f);
	}
	vec2 SizeTop() const
	{
		dbg_assert(m_Inited, "Tried to get size of uninited nameplate");
		// X: Total width including padding of line, Y: Max height of line parts
		vec2 LineSize = vec2(0.0f, 0.0f);
		float WMax = 0.0f;
		float HTotal = 0.0f;
		bool Empty = true;
		for(auto PartIt = m_vpPartsTop.begin(); PartIt != m_vpPartsTop.end(); ++PartIt) // NOLINT(modernize-loop-convert) For consistency with Render
		{
			CNamePlatePart &Part = **PartIt;
			if(Part.NewLine())
			{
				if(!Empty)
				{
					if(LineSize.x > WMax)
						WMax = LineSize.x;
					HTotal += LineSize.y;
				}
				LineSize = vec2(0.0f, 0.0f);
			}
			else if(Part.Visible() || Part.ShiftOnInvis())
			{
				Empty = false;
				LineSize.x += Part.Size().x + Part.Padding().x;
				LineSize.y = std::max(LineSize.y, Part.Size().y + Part.Padding().y);
			}
		}
		if(LineSize.x > WMax)
			WMax = LineSize.x;
		HTotal += LineSize.y;
		return vec2(WMax, HTotal);
	}
	vec2 SizeBottom() const
	{
		dbg_assert(m_Inited, "Tried to get size of uninited nameplate");
		// X: Total width including padding of line, Y: Max height of line parts
		vec2 LineSize = vec2(0.0f, 0.0f);
		float WMax = 0.0f;
		float HTotal = 0.0f;
		bool Empty = true;
		for(auto PartIt = m_vpPartsBottom.begin(); PartIt != m_vpPartsBottom.end(); ++PartIt) // NOLINT(modernize-loop-convert) For consistency with Render
		{
			CNamePlatePart &Part = **PartIt;
			if(Part.NewLine())
			{
				if(!Empty)
				{
					if(LineSize.x > WMax)
						WMax = LineSize.x;
					HTotal += LineSize.y;
				}
				LineSize = vec2(0.0f, 0.0f);
			}
			else if(Part.Visible() || Part.ShiftOnInvis())
			{
				Empty = false;
				LineSize.x += Part.Size().x + Part.Padding().x;
				LineSize.y = std::max(LineSize.y, Part.Size().y + Part.Padding().y);
			}
		}
		if(LineSize.x > WMax)
			WMax = LineSize.x;
		HTotal += LineSize.y;
		return vec2(WMax, HTotal);
	}
};

class CNamePlates::CNamePlatesData
{
public:
	CNamePlate m_aNamePlates[MAX_CLIENTS];
};

void CNamePlates::RenderNamePlateGame(vec2 Position, const CNetObj_PlayerInfo *pPlayerInfo, float Alpha)
{
	// Get screen edges to avoid rendering offscreen
	float ScreenX0, ScreenY0, ScreenX1, ScreenY1;
	Graphics()->GetScreen(&ScreenX0, &ScreenY0, &ScreenX1, &ScreenY1);

	// Assume that the name plate fits into a 800x800 box placed directly above the tee
	ScreenX0 -= 400;
	ScreenX1 += 400;
	ScreenY1 += 800;
	if(!(in_range(Position.x, ScreenX0, ScreenX1) && in_range(Position.y, ScreenY0, ScreenY1)))
		return;

	CNamePlateData Data;

	const auto &ClientData = GameClient()->m_aClients[pPlayerInfo->m_ClientId];
	const bool OtherTeam = GameClient()->IsOtherTeam(pPlayerInfo->m_ClientId);

	Data.m_InGame = true;

	Data.m_ShowName = pPlayerInfo->m_Local ? g_Config.m_ClNamePlatesOwn : g_Config.m_ClNamePlates;
	str_copy(Data.m_aName, GameClient()->m_aClients[pPlayerInfo->m_ClientId].m_aName);
	Data.m_ShowFriendMark = Data.m_ShowName && g_Config.m_ClNamePlatesFriendMark && GameClient()->m_aClients[pPlayerInfo->m_ClientId].m_Friend;
	Data.m_ShowClientId = Data.m_ShowName && (g_Config.m_Debug || g_Config.m_ClNamePlatesIds);
	Data.m_FontSize = 18.0f + 20.0f * g_Config.m_ClNamePlatesSize / 100.0f;

	Data.m_ClientId = pPlayerInfo->m_ClientId;
	Data.m_ClientIdSeparateLine = g_Config.m_ClNamePlatesIdsSeparateLine;
	Data.m_FontSizeClientId = Data.m_ClientIdSeparateLine ? (18.0f + 20.0f * g_Config.m_ClNamePlatesIdsSize / 100.0f) : Data.m_FontSize;

	Data.m_ShowClan = Data.m_ShowName && g_Config.m_ClNamePlatesClan;
	str_copy(Data.m_aClan, GameClient()->m_aClients[pPlayerInfo->m_ClientId].m_aClan);
	Data.m_FontSizeClan = 18.0f + 20.0f * g_Config.m_ClNamePlatesClanSize / 100.0f;

	Data.m_FontSizeHookStrongWeak = 18.0f + 20.0f * g_Config.m_ClNamePlatesStrongSize / 100.0f;
	Data.m_FontSizeDirection = 18.0f + 20.0f * g_Config.m_ClDirectionSize / 100.0f;

	if(g_Config.m_ClNamePlatesAlways == 0)
		Alpha *= std::clamp(1.0f - std::pow(distance(GameClient()->m_Controls.m_aTargetPos[g_Config.m_ClDummy], Position) / 200.0f, 16.0f), 0.0f, 1.0f);
	if(OtherTeam)
		Alpha *= (float)g_Config.m_ClShowOthersAlpha / 100.0f;

	Data.m_Color = ColorRGBA(1.0f, 1.0f, 1.0f);
	if(g_Config.m_ClNamePlatesTeamcolors)
	{
		if(GameClient()->IsTeamPlay())
		{
			if(ClientData.m_Team == TEAM_RED)
				Data.m_Color = ColorRGBA(1.0f, 0.5f, 0.5f);
			else if(ClientData.m_Team == TEAM_BLUE)
				Data.m_Color = ColorRGBA(0.7f, 0.7f, 1.0f);
		}
		else
		{
			const int Team = GameClient()->m_Teams.Team(pPlayerInfo->m_ClientId);
			if(Team)
				Data.m_Color = GameClient()->GetDDTeamColor(Team, 0.75f);
		}
	}
	Data.m_Color.a = Alpha;

	int ShowDirectionConfig = g_Config.m_ClShowDirection;
#if defined(CONF_VIDEORECORDER)
	if(IVideo::Current())
		ShowDirectionConfig = g_Config.m_ClVideoShowDirection;
#endif
	Data.m_DirLeft = Data.m_DirJump = Data.m_DirRight = false;
	switch(ShowDirectionConfig)
	{
	case 0: // Off
		Data.m_ShowDirection = false;
		break;
	case 1: // Others
		Data.m_ShowDirection = !pPlayerInfo->m_Local;
		break;
	case 2: // Everyone
		Data.m_ShowDirection = true;
		break;
	case 3: // Only self
		Data.m_ShowDirection = pPlayerInfo->m_Local;
		break;
	default:
		dbg_assert_failed("ShowDirectionConfig invalid");
	}
	if(Data.m_ShowDirection)
	{
		if(Client()->State() != IClient::STATE_DEMOPLAYBACK &&
			pPlayerInfo->m_ClientId == GameClient()->m_aLocalIds[!g_Config.m_ClDummy])
		{
			const auto &InputData = GameClient()->m_Controls.m_aInputData[!g_Config.m_ClDummy];
			Data.m_DirLeft = InputData.m_Direction == -1;
			Data.m_DirJump = InputData.m_Jump == 1;
			Data.m_DirRight = InputData.m_Direction == 1;
		}
		else if(Client()->State() != IClient::STATE_DEMOPLAYBACK && pPlayerInfo->m_Local) // Always render local input when not in demo playback
		{
			const auto &InputData = GameClient()->m_Controls.m_aInputData[g_Config.m_ClDummy];
			Data.m_DirLeft = InputData.m_Direction == -1;
			Data.m_DirJump = InputData.m_Jump == 1;
			Data.m_DirRight = InputData.m_Direction == 1;
		}
		else
		{
			const auto &Character = GameClient()->m_Snap.m_aCharacters[pPlayerInfo->m_ClientId];
			Data.m_DirLeft = Character.m_Cur.m_Direction == -1;
			Data.m_DirJump = Character.m_Cur.m_Jumped & 1;
			Data.m_DirRight = Character.m_Cur.m_Direction == 1;
		}
	}

	Data.m_ShowHookStrongWeak = false;
	Data.m_HookStrongWeakState = EHookStrongWeakState::NEUTRAL;
	Data.m_ShowHookStrongWeakId = false;
	Data.m_HookStrongWeakId = 0;

	const bool Following = (GameClient()->m_Snap.m_SpecInfo.m_Active && !GameClient()->m_MultiViewActivated && GameClient()->m_Snap.m_SpecInfo.m_SpectatorId != SPEC_FREEVIEW);
	if(GameClient()->m_Snap.m_LocalClientId != -1 || Following)
	{
		const int SelectedId = Following ? GameClient()->m_Snap.m_SpecInfo.m_SpectatorId : GameClient()->m_Snap.m_LocalClientId;
		const CGameClient::CSnapState::CCharacterInfo &Selected = GameClient()->m_Snap.m_aCharacters[SelectedId];
		const CGameClient::CSnapState::CCharacterInfo &Other = GameClient()->m_Snap.m_aCharacters[pPlayerInfo->m_ClientId];

		if((Selected.m_HasExtendedData || GameClient()->m_aClients[SelectedId].m_SpecCharPresent) && Other.m_HasExtendedData)
		{
			int SelectedStrongWeakId = Selected.m_HasExtendedData ? Selected.m_ExtendedData.m_StrongWeakId : 0;
			Data.m_HookStrongWeakId = Other.m_ExtendedData.m_StrongWeakId;
			Data.m_ShowHookStrongWeakId = g_Config.m_Debug || g_Config.m_ClNamePlatesStrong == 2;
			if(SelectedId == pPlayerInfo->m_ClientId)
				Data.m_ShowHookStrongWeak = Data.m_ShowHookStrongWeakId;
			else
			{
				Data.m_HookStrongWeakState = SelectedStrongWeakId > Other.m_ExtendedData.m_StrongWeakId ? EHookStrongWeakState::STRONG : EHookStrongWeakState::WEAK;
				Data.m_ShowHookStrongWeak = g_Config.m_Debug || g_Config.m_ClNamePlatesStrong > 0;
			}
		}
	}

	// TClient
	if(g_Config.m_TcWarList && g_Config.m_TcWarListShowClan && GameClient()->m_WarList.GetWarData(pPlayerInfo->m_ClientId).m_WarClan)
		Data.m_ShowClan = true;

	Data.m_Local = pPlayerInfo->m_Local;

	// CMClient
	Data.m_CheckpointsEnabled = m_CheckpointsEnabled;

	// Check if the nameplate is actually on screen
	CNamePlate &NamePlate = m_pData->m_aNamePlates[pPlayerInfo->m_ClientId];
	NamePlate.Update(*GameClient(), Data);
	NamePlate.RenderTop(*GameClient(), Position - vec2(0.0f, (float)g_Config.m_ClNamePlatesOffset));
	NamePlate.RenderBottom(*GameClient(), Position + vec2(0.0f, (float)g_Config.m_ClNamePlatesOffset + 28.0f - 5.0f));
}

void CNamePlates::RenderNamePlatePreview(vec2 Position, int Dummy)
{
	const float FontSize = 18.0f + 20.0f * g_Config.m_ClNamePlatesSize / 100.0f;
	const float FontSizeClan = 18.0f + 20.0f * g_Config.m_ClNamePlatesClanSize / 100.0f;

	const float FontSizeDirection = 18.0f + 20.0f * g_Config.m_ClDirectionSize / 100.0f;
	const float FontSizeHookStrongWeak = 18.0f + 20.0f * g_Config.m_ClNamePlatesStrongSize / 100.0f;

	CNamePlateData Data;

	Data.m_InGame = false;
	Data.m_Color = g_Config.m_ClNamePlatesTeamcolors ? GameClient()->GetDDTeamColor(13, 0.75f) : TextRender()->DefaultTextColor();
	Data.m_Color.a = 1.0f;

	Data.m_ShowName = g_Config.m_ClNamePlates || g_Config.m_ClNamePlatesOwn;
	const char *pName = Dummy == 0 ? Client()->PlayerName() : Client()->DummyName();
	str_copy(Data.m_aName, str_utf8_skip_whitespaces(pName));
	str_utf8_trim_right(Data.m_aName);
	Data.m_FontSize = FontSize;

	Data.m_ShowFriendMark = Data.m_ShowName && g_Config.m_ClNamePlatesFriendMark;

	Data.m_ShowClientId = Data.m_ShowName && (g_Config.m_Debug || g_Config.m_ClNamePlatesIds);
	Data.m_ClientId = Dummy;
	Data.m_ClientIdSeparateLine = g_Config.m_ClNamePlatesIdsSeparateLine;
	Data.m_FontSizeClientId = Data.m_ClientIdSeparateLine ? (18.0f + 20.0f * g_Config.m_ClNamePlatesIdsSize / 100.0f) : Data.m_FontSize;

	Data.m_ShowClan = Data.m_ShowName && g_Config.m_ClNamePlatesClan;
	const char *pClan = Dummy == 0 ? g_Config.m_PlayerClan : g_Config.m_ClDummyClan;
	str_copy(Data.m_aClan, str_utf8_skip_whitespaces(pClan));
	str_utf8_trim_right(Data.m_aClan);
	if(Data.m_aClan[0] == '\0')
		str_copy(Data.m_aClan, "Clan Name");
	Data.m_FontSizeClan = FontSizeClan;

	Data.m_ShowDirection = g_Config.m_ClShowDirection != 0 ? true : false;
	Data.m_DirLeft = Data.m_DirJump = Data.m_DirRight = true;
	Data.m_FontSizeDirection = FontSizeDirection;

	Data.m_FontSizeHookStrongWeak = FontSizeHookStrongWeak;
	Data.m_HookStrongWeakId = Data.m_ClientId;
	Data.m_ShowHookStrongWeakId = g_Config.m_ClNamePlatesStrong == 2;
	if(Dummy == g_Config.m_ClDummy)
	{
		Data.m_HookStrongWeakState = EHookStrongWeakState::NEUTRAL;
		Data.m_ShowHookStrongWeak = Data.m_ShowHookStrongWeakId;
	}
	else
	{
		Data.m_HookStrongWeakState = Data.m_HookStrongWeakId == 2 ? EHookStrongWeakState::STRONG : EHookStrongWeakState::WEAK;
		Data.m_ShowHookStrongWeak = g_Config.m_ClNamePlatesStrong > 0;
	}

	// TClient
	Data.m_Local = false;

	CTeeRenderInfo TeeRenderInfo;
	if(Dummy == 0)
	{
		TeeRenderInfo.Apply(GameClient()->m_Skins.Find(g_Config.m_ClPlayerSkin));
		TeeRenderInfo.ApplyColors(g_Config.m_ClPlayerUseCustomColor, g_Config.m_ClPlayerColorBody, g_Config.m_ClPlayerColorFeet);
	}
	else
	{
		TeeRenderInfo.Apply(GameClient()->m_Skins.Find(g_Config.m_ClDummySkin));
		TeeRenderInfo.ApplyColors(g_Config.m_ClDummyUseCustomColor, g_Config.m_ClDummyColorBody, g_Config.m_ClDummyColorFeet);
	}
	TeeRenderInfo.m_Size = 64.0f;

	CNamePlate NamePlate(*GameClient(), Data);
	Position.y += NamePlate.SizeTop().y / 2.0f;
	Position.y += (float)g_Config.m_ClNamePlatesOffset / 2.0f;
	// tee looking towards cursor, and it is happy when you touch it
	const vec2 DeltaPosition = Ui()->MousePos() - Position;
	const float Distance = length(DeltaPosition);
	const float InteractionDistance = 20.0f;
	const vec2 TeeDirection = Distance < InteractionDistance ? normalize(vec2(DeltaPosition.x, maximum(DeltaPosition.y, 0.5f))) : normalize(DeltaPosition);
	const int TeeEmote = Distance < InteractionDistance ? EMOTE_HAPPY : (Dummy ? g_Config.m_ClDummyDefaultEyes : g_Config.m_ClPlayerDefaultEyes);
	RenderTools()->RenderTee(CAnimState::GetIdle(), &TeeRenderInfo, TeeEmote, TeeDirection, Position);
	NamePlate.RenderTop(*GameClient(), Position - vec2(0.0f, (float)g_Config.m_ClNamePlatesOffset));
	NamePlate.RenderBottom(*GameClient(), Position + vec2(0.0f, (float)g_Config.m_ClNamePlatesOffset + 28.0f - 5.0f));
	NamePlate.Reset(*GameClient());
}

void CNamePlates::ResetNamePlates()
{
	for(CNamePlate &NamePlate : m_pData->m_aNamePlates)
		NamePlate.Reset(*GameClient());
}

void CNamePlates::OnRender()
{
	if(Client()->State() != IClient::STATE_ONLINE && Client()->State() != IClient::STATE_DEMOPLAYBACK)
		return;

	int ShowDirection = g_Config.m_ClShowDirection;
#if defined(CONF_VIDEORECORDER)
	if(IVideo::Current())
		ShowDirection = g_Config.m_ClVideoShowDirection;
#endif
	if(!g_Config.m_ClNamePlates && ShowDirection == 0)
		return;

	for(int i = 0; i < MAX_CLIENTS; i++)
	{
		const CNetObj_PlayerInfo *pInfo = GameClient()->m_Snap.m_apPlayerInfos[i];
		if(!pInfo)
			continue;

		// Each player can also have a spectator char whose name plate is displayed independently
		if(GameClient()->m_aClients[i].m_SpecCharPresent)
		{
			const vec2 RenderPos = GameClient()->m_aClients[i].m_SpecChar;
			RenderNamePlateGame(RenderPos, pInfo, 0.4f);
		}
		// Only render name plates for active characters
		if(GameClient()->m_Snap.m_aCharacters[i].m_Active)
		{
			// TClient
			if(GameClient()->m_aClients[i].m_IsVolleyBall)
				continue;
			// if(g_Config.m_TcRenderNameplateSpec > 0)
			//	continue;
			const vec2 RenderPos = GameClient()->m_aClients[i].m_RenderPos;
			RenderNamePlateGame(RenderPos, pInfo, 1.0f);
		}
	}
}

void CNamePlates::OnMapLoad()
{
	m_CheckpointsEnabled = false;
	for(int i = 0; i < Collision()->GetWidth() * Collision()->GetHeight(); i++)
	{
		if(Collision()->IsTeleCheckpoint(i))
		{
			m_CheckpointsEnabled = true;
			break;
		}
	}
}

void CNamePlates::OnWindowResize()
{
	ResetNamePlates();
}

CNamePlates::CNamePlates() :
	m_pData(new CNamePlates::CNamePlatesData()) {}

CNamePlates::~CNamePlates()
{
	delete m_pData;
}
