1#include "nameplates.h"
2
3#include <engine/font_icons.h>
4#include <engine/graphics.h>
5#include <engine/shared/config.h>
6#include <engine/shared/protocol7.h>
7#include <engine/textrender.h>
8
9#include <generated/client_data.h>
10
11#include <game/client/animstate.h>
12#include <game/client/gameclient.h>
13#include <game/client/prediction/entities/character.h>
14
15#include <memory>
16#include <vector>
17
18enum class EHookStrongWeakState
19{
20 WEAK,
21 NEUTRAL,
22 STRONG
23};
24
25class CNamePlateData
26{
27public:
28 bool m_InGame;
29 ColorRGBA m_Color;
30 bool m_ShowName;
31 char m_aName[std::max<size_t>(a: MAX_NAME_LENGTH, b: protocol7::MAX_NAME_ARRAY_SIZE)];
32 bool m_ShowFriendMark;
33 bool m_ShowClientId;
34 int m_ClientId;
35 float m_FontSizeClientId;
36 bool m_ClientIdSeparateLine;
37 float m_FontSize;
38 bool m_ShowClan;
39 char m_aClan[std::max<size_t>(a: MAX_CLAN_LENGTH, b: protocol7::MAX_CLAN_ARRAY_SIZE)];
40 float m_FontSizeClan;
41 bool m_ShowDirection;
42 bool m_DirLeft;
43 bool m_DirJump;
44 bool m_DirRight;
45 float m_FontSizeDirection;
46 bool m_ShowHookStrongWeak;
47 EHookStrongWeakState m_HookStrongWeakState;
48 bool m_ShowHookStrongWeakId;
49 int m_HookStrongWeakId;
50 float m_FontSizeHookStrongWeak;
51};
52
53// Part Types
54
55static constexpr float DEFAULT_PADDING = 5.0f;
56
57class CNamePlatePart
58{
59protected:
60 vec2 m_Size = vec2(0.0f, 0.0f);
61 vec2 m_Padding = vec2(DEFAULT_PADDING, DEFAULT_PADDING);
62 bool m_NewLine = false; // Whether this part is a new line (doesn't do anything else)
63 bool m_Visible = true; // Whether this part is visible
64 bool m_ShiftOnInvis = false; // Whether when not visible will still take up space
65 CNamePlatePart(CGameClient &This) {}
66
67public:
68 virtual void Update(CGameClient &This, const CNamePlateData &Data) {}
69 virtual void Reset(CGameClient &This) {}
70 virtual void Render(CGameClient &This, vec2 Pos) const {}
71 vec2 Size() const { return m_Size; }
72 vec2 Padding() const { return m_Padding; }
73 bool NewLine() const { return m_NewLine; }
74 bool Visible() const { return m_Visible; }
75 bool ShiftOnInvis() const { return m_ShiftOnInvis; }
76 CNamePlatePart() = delete;
77 virtual ~CNamePlatePart() = default;
78};
79
80using PartsVector = std::vector<std::unique_ptr<CNamePlatePart>>;
81
82class CNamePlatePartText : public CNamePlatePart
83{
84protected:
85 STextContainerIndex m_TextContainerIndex;
86 virtual bool UpdateNeeded(CGameClient &This, const CNamePlateData &Data) = 0;
87 virtual void UpdateText(CGameClient &This, const CNamePlateData &Data) = 0;
88 ColorRGBA m_Color = ColorRGBA(1.0f, 1.0f, 1.0f, 1.0f);
89 CNamePlatePartText(CGameClient &This) :
90 CNamePlatePart(This)
91 {
92 Reset(This);
93 }
94
95public:
96 void Update(CGameClient &This, const CNamePlateData &Data) override
97 {
98 if(!UpdateNeeded(This, Data) && m_TextContainerIndex.Valid())
99 return;
100
101 // Set flags
102 unsigned int Flags = ETextRenderFlags::TEXT_RENDER_FLAG_NO_FIRST_CHARACTER_X_BEARING | ETextRenderFlags::TEXT_RENDER_FLAG_NO_LAST_CHARACTER_ADVANCE;
103 if(Data.m_InGame)
104 Flags |= ETextRenderFlags::TEXT_RENDER_FLAG_NO_PIXEL_ALIGNMENT; // Prevent jittering from rounding
105 This.TextRender()->SetRenderFlags(Flags);
106
107 if(Data.m_InGame)
108 {
109 // Create text at standard zoom
110 float ScreenX0, ScreenY0, ScreenX1, ScreenY1;
111 This.Graphics()->GetScreen(pTopLeftX: &ScreenX0, pTopLeftY: &ScreenY0, pBottomRightX: &ScreenX1, pBottomRightY: &ScreenY1);
112 This.Graphics()->MapScreenToInterface(CenterX: This.m_Camera.m_Center.x, CenterY: This.m_Camera.m_Center.y);
113 This.TextRender()->DeleteTextContainer(TextContainerIndex&: m_TextContainerIndex);
114 UpdateText(This, Data);
115 This.Graphics()->MapScreen(TopLeftX: ScreenX0, TopLeftY: ScreenY0, BottomRightX: ScreenX1, BottomRightY: ScreenY1);
116 }
117 else
118 {
119 UpdateText(This, Data);
120 }
121
122 This.TextRender()->SetRenderFlags(0);
123
124 if(!m_TextContainerIndex.Valid())
125 {
126 m_Visible = false;
127 return;
128 }
129
130 const STextBoundingBox Container = This.TextRender()->GetBoundingBoxTextContainer(TextContainerIndex: m_TextContainerIndex);
131 m_Size = vec2(Container.m_W, Container.m_H);
132 }
133 void Reset(CGameClient &This) override
134 {
135 This.TextRender()->DeleteTextContainer(TextContainerIndex&: m_TextContainerIndex);
136 }
137 void Render(CGameClient &This, vec2 Pos) const override
138 {
139 if(!m_TextContainerIndex.Valid())
140 return;
141
142 ColorRGBA OutlineColor, Color;
143 Color = m_Color;
144 OutlineColor = ColorRGBA(0.0f, 0.0f, 0.0f, 0.5f * m_Color.a);
145 This.TextRender()->RenderTextContainer(TextContainerIndex: m_TextContainerIndex,
146 TextColor: Color, TextOutlineColor: OutlineColor,
147 X: Pos.x - Size().x / 2.0f, Y: Pos.y - Size().y / 2.0f);
148 }
149};
150
151class CNamePlatePartIcon : public CNamePlatePart
152{
153protected:
154 IGraphics::CTextureHandle m_Texture;
155 float m_Rotation = 0.0f;
156 ColorRGBA m_Color = ColorRGBA(1.0f, 1.0f, 1.0f, 1.0f);
157 CNamePlatePartIcon(CGameClient &This) :
158 CNamePlatePart(This) {}
159
160public:
161 void Render(CGameClient &This, vec2 Pos) const override
162 {
163 IGraphics::CQuadItem QuadItem(Pos.x - Size().x / 2.0f, Pos.y - Size().y / 2.0f, Size().x, Size().y);
164 This.Graphics()->TextureSet(Texture: m_Texture);
165 This.Graphics()->QuadsBegin();
166 This.Graphics()->SetColor(m_Color);
167 This.Graphics()->QuadsSetRotation(Angle: m_Rotation);
168 This.Graphics()->QuadsDrawTL(pArray: &QuadItem, Num: 1);
169 This.Graphics()->QuadsEnd();
170 This.Graphics()->QuadsSetRotation(Angle: 0.0f);
171 }
172};
173
174class CNamePlatePartSprite : public CNamePlatePart
175{
176protected:
177 IGraphics::CTextureHandle m_Texture;
178 int m_Sprite = -1;
179 int m_SpriteFlags = 0;
180 float m_Rotation = 0.0f;
181 ColorRGBA m_Color = ColorRGBA(1.0f, 1.0f, 1.0f, 1.0f);
182 CNamePlatePartSprite(CGameClient &This) :
183 CNamePlatePart(This) {}
184
185public:
186 void Render(CGameClient &This, vec2 Pos) const override
187 {
188 This.Graphics()->TextureSet(Texture: m_Texture);
189 This.Graphics()->QuadsSetRotation(Angle: m_Rotation);
190 This.Graphics()->QuadsBegin();
191 This.Graphics()->SetColor(m_Color);
192 This.Graphics()->SelectSprite(Id: m_Sprite, Flags: m_SpriteFlags);
193 This.Graphics()->DrawSprite(x: Pos.x, y: Pos.y, ScaledWidth: Size().x, ScaledHeight: Size().y);
194 This.Graphics()->QuadsEnd();
195 This.Graphics()->QuadsSetRotation(Angle: 0.0f);
196 }
197};
198
199// Part Definitions
200
201class CNamePlatePartNewLine : public CNamePlatePart
202{
203public:
204 CNamePlatePartNewLine(CGameClient &This) :
205 CNamePlatePart(This)
206 {
207 m_NewLine = true;
208 }
209};
210
211enum Direction
212{
213 DIRECTION_LEFT,
214 DIRECTION_UP,
215 DIRECTION_RIGHT
216};
217
218class CNamePlatePartDirection : public CNamePlatePartIcon
219{
220private:
221 int m_Direction;
222
223public:
224 CNamePlatePartDirection(CGameClient &This, Direction Dir) :
225 CNamePlatePartIcon(This)
226 {
227 m_Texture = g_pData->m_aImages[IMAGE_ARROW].m_Id;
228 m_Direction = Dir;
229 switch(m_Direction)
230 {
231 case DIRECTION_LEFT:
232 m_Rotation = pi;
233 break;
234 case DIRECTION_UP:
235 m_Rotation = pi / -2.0f;
236 break;
237 case DIRECTION_RIGHT:
238 m_Rotation = 0.0f;
239 break;
240 }
241 }
242 void Update(CGameClient &This, const CNamePlateData &Data) override
243 {
244 if(!Data.m_ShowDirection)
245 {
246 m_ShiftOnInvis = false;
247 m_Visible = false;
248 return;
249 }
250 m_ShiftOnInvis = true; // Only shift (horizontally) the other parts if directions as a whole is visible
251 m_Size = vec2(Data.m_FontSizeDirection, Data.m_FontSizeDirection);
252 m_Padding.y = m_Size.y / 2.0f;
253 switch(m_Direction)
254 {
255 case DIRECTION_LEFT:
256 m_Visible = Data.m_DirLeft;
257 break;
258 case DIRECTION_UP:
259 m_Visible = Data.m_DirJump;
260 break;
261 case DIRECTION_RIGHT:
262 m_Visible = Data.m_DirRight;
263 break;
264 }
265 m_Color.a = Data.m_Color.a;
266 }
267};
268
269class CNamePlatePartClientId : public CNamePlatePartText
270{
271private:
272 int m_ClientId = -1;
273 static_assert(MAX_CLIENTS <= 999, "Make this buffer bigger");
274 char m_aText[5] = "";
275 float m_FontSize = -INFINITY;
276 bool m_ClientIdSeparateLine = false;
277
278protected:
279 bool UpdateNeeded(CGameClient &This, const CNamePlateData &Data) override
280 {
281 m_Visible = Data.m_ShowClientId && (Data.m_ClientIdSeparateLine == m_ClientIdSeparateLine);
282 if(!m_Visible)
283 return false;
284 m_Color = Data.m_Color;
285 return m_FontSize != Data.m_FontSizeClientId || m_ClientId != Data.m_ClientId;
286 }
287 void UpdateText(CGameClient &This, const CNamePlateData &Data) override
288 {
289 m_FontSize = Data.m_FontSizeClientId;
290 m_ClientId = Data.m_ClientId;
291 if(m_ClientIdSeparateLine)
292 str_format(buffer: m_aText, buffer_size: sizeof(m_aText), format: "%d", m_ClientId);
293 else
294 str_format(buffer: m_aText, buffer_size: sizeof(m_aText), format: "%d:", m_ClientId);
295 CTextCursor Cursor;
296 Cursor.m_FontSize = m_FontSize;
297 This.TextRender()->CreateOrAppendTextContainer(TextContainerIndex&: m_TextContainerIndex, pCursor: &Cursor, pText: m_aText);
298 }
299
300public:
301 CNamePlatePartClientId(CGameClient &This, bool ClientIdSeparateLine) :
302 CNamePlatePartText(This)
303 {
304 m_ClientIdSeparateLine = ClientIdSeparateLine;
305 }
306};
307
308class CNamePlatePartFriendMark : public CNamePlatePartText
309{
310private:
311 float m_FontSize = -INFINITY;
312
313protected:
314 bool UpdateNeeded(CGameClient &This, const CNamePlateData &Data) override
315 {
316 m_Visible = Data.m_ShowFriendMark;
317 if(!m_Visible)
318 return false;
319 m_Color.a = Data.m_Color.a;
320 return m_FontSize != Data.m_FontSize;
321 }
322 void UpdateText(CGameClient &This, const CNamePlateData &Data) override
323 {
324 m_FontSize = Data.m_FontSize;
325 CTextCursor Cursor;
326 This.TextRender()->SetFontPreset(EFontPreset::ICON_FONT);
327 Cursor.m_FontSize = m_FontSize;
328 This.TextRender()->CreateOrAppendTextContainer(TextContainerIndex&: m_TextContainerIndex, pCursor: &Cursor, pText: FontIcon::HEART);
329 This.TextRender()->SetFontPreset(EFontPreset::DEFAULT_FONT);
330 }
331
332public:
333 CNamePlatePartFriendMark(CGameClient &This) :
334 CNamePlatePartText(This)
335 {
336 m_Color = ColorRGBA(1.0f, 0.0f, 0.0f);
337 }
338};
339
340class CNamePlatePartName : public CNamePlatePartText
341{
342private:
343 char m_aText[std::max<size_t>(a: MAX_NAME_LENGTH, b: protocol7::MAX_NAME_ARRAY_SIZE)] = "";
344 float m_FontSize = -INFINITY;
345
346protected:
347 bool UpdateNeeded(CGameClient &This, const CNamePlateData &Data) override
348 {
349 m_Visible = Data.m_ShowName;
350 if(!m_Visible)
351 return false;
352 m_Color = Data.m_Color;
353 return m_FontSize != Data.m_FontSize || str_comp(a: m_aText, b: Data.m_aName) != 0;
354 }
355 void UpdateText(CGameClient &This, const CNamePlateData &Data) override
356 {
357 m_FontSize = Data.m_FontSize;
358 str_copy(dst: m_aText, src: Data.m_aName, dst_size: sizeof(m_aText));
359 CTextCursor Cursor;
360 Cursor.m_FontSize = m_FontSize;
361 This.TextRender()->CreateOrAppendTextContainer(TextContainerIndex&: m_TextContainerIndex, pCursor: &Cursor, pText: m_aText);
362 }
363
364public:
365 CNamePlatePartName(CGameClient &This) :
366 CNamePlatePartText(This) {}
367};
368
369class CNamePlatePartClan : public CNamePlatePartText
370{
371private:
372 char m_aText[std::max<size_t>(a: MAX_CLAN_LENGTH, b: protocol7::MAX_CLAN_ARRAY_SIZE)] = "";
373 float m_FontSize = -INFINITY;
374
375protected:
376 bool UpdateNeeded(CGameClient &This, const CNamePlateData &Data) override
377 {
378 m_Visible = Data.m_ShowClan;
379 if(!m_Visible && Data.m_aClan[0] != '\0')
380 return false;
381 m_Color = Data.m_Color;
382 return m_FontSize != Data.m_FontSizeClan || str_comp(a: m_aText, b: Data.m_aClan) != 0;
383 }
384 void UpdateText(CGameClient &This, const CNamePlateData &Data) override
385 {
386 m_FontSize = Data.m_FontSizeClan;
387 str_copy(dst: m_aText, src: Data.m_aClan, dst_size: sizeof(m_aText));
388 CTextCursor Cursor;
389 Cursor.m_FontSize = m_FontSize;
390 This.TextRender()->CreateOrAppendTextContainer(TextContainerIndex&: m_TextContainerIndex, pCursor: &Cursor, pText: m_aText);
391 }
392
393public:
394 CNamePlatePartClan(CGameClient &This) :
395 CNamePlatePartText(This) {}
396};
397
398class CNamePlatePartHookStrongWeak : public CNamePlatePartSprite
399{
400protected:
401 void Update(CGameClient &This, const CNamePlateData &Data) override
402 {
403 m_Visible = Data.m_ShowHookStrongWeak;
404 if(!m_Visible)
405 return;
406 m_Size = vec2(Data.m_FontSizeHookStrongWeak + DEFAULT_PADDING, Data.m_FontSizeHookStrongWeak + DEFAULT_PADDING);
407 switch(Data.m_HookStrongWeakState)
408 {
409 case EHookStrongWeakState::STRONG:
410 m_Sprite = SPRITE_HOOK_STRONG;
411 m_Color = color_cast<ColorRGBA>(hsl: ColorHSLA(6401973));
412 break;
413 case EHookStrongWeakState::NEUTRAL:
414 m_Sprite = SPRITE_HOOK_ICON;
415 m_Color = ColorRGBA(1.0f, 1.0f, 1.0f);
416 break;
417 case EHookStrongWeakState::WEAK:
418 m_Sprite = SPRITE_HOOK_WEAK;
419 m_Color = color_cast<ColorRGBA>(hsl: ColorHSLA(41131));
420 break;
421 }
422 m_Color.a = Data.m_Color.a;
423 }
424
425public:
426 CNamePlatePartHookStrongWeak(CGameClient &This) :
427 CNamePlatePartSprite(This)
428 {
429 m_Texture = g_pData->m_aImages[IMAGE_STRONGWEAK].m_Id;
430 m_Padding = vec2(0.0f, 0.0f);
431 }
432};
433
434class CNamePlatePartHookStrongWeakId : public CNamePlatePartText
435{
436private:
437 int m_StrongWeakId = -1;
438 static_assert(MAX_CLIENTS <= 999, "Make this buffer bigger");
439 char m_aText[4] = "";
440 float m_FontSize = -INFINITY;
441
442protected:
443 bool UpdateNeeded(CGameClient &This, const CNamePlateData &Data) override
444 {
445 m_Visible = Data.m_ShowHookStrongWeakId;
446 if(!m_Visible)
447 return false;
448 switch(Data.m_HookStrongWeakState)
449 {
450 case EHookStrongWeakState::STRONG:
451 m_Color = color_cast<ColorRGBA>(hsl: ColorHSLA(6401973));
452 break;
453 case EHookStrongWeakState::NEUTRAL:
454 m_Color = ColorRGBA(1.0f, 1.0f, 1.0f);
455 break;
456 case EHookStrongWeakState::WEAK:
457 m_Color = color_cast<ColorRGBA>(hsl: ColorHSLA(41131));
458 break;
459 }
460 m_Color.a = Data.m_Color.a;
461 return m_FontSize != Data.m_FontSizeHookStrongWeak || m_StrongWeakId != Data.m_HookStrongWeakId;
462 }
463 void UpdateText(CGameClient &This, const CNamePlateData &Data) override
464 {
465 m_FontSize = Data.m_FontSizeHookStrongWeak;
466 m_StrongWeakId = Data.m_HookStrongWeakId;
467 str_format(buffer: m_aText, buffer_size: sizeof(m_aText), format: "%d", m_StrongWeakId);
468 CTextCursor Cursor;
469 Cursor.m_FontSize = m_FontSize;
470 This.TextRender()->CreateOrAppendTextContainer(TextContainerIndex&: m_TextContainerIndex, pCursor: &Cursor, pText: m_aText);
471 }
472
473public:
474 CNamePlatePartHookStrongWeakId(CGameClient &This) :
475 CNamePlatePartText(This) {}
476};
477
478// Name Plates
479
480class CNamePlate
481{
482private:
483 bool m_Inited = false;
484 bool m_InGame = false;
485 PartsVector m_vpParts;
486 void RenderLine(CGameClient &This,
487 vec2 Pos, vec2 Size,
488 PartsVector::iterator Start, PartsVector::iterator End)
489 {
490 Pos.x -= Size.x / 2.0f;
491 for(auto PartIt = Start; PartIt != End; ++PartIt)
492 {
493 const CNamePlatePart &Part = **PartIt;
494 if(Part.Visible())
495 {
496 Part.Render(This, Pos: vec2(
497 Pos.x + (Part.Padding().x + Part.Size().x) / 2.0f,
498 Pos.y - std::max(a: Size.y, b: Part.Padding().y + Part.Size().y) / 2.0f));
499 }
500 if(Part.Visible() || Part.ShiftOnInvis())
501 Pos.x += Part.Size().x + Part.Padding().x;
502 }
503 }
504 template<typename PartType, typename... ArgsType>
505 void AddPart(CGameClient &This, ArgsType &&...Args)
506 {
507 m_vpParts.push_back(std::make_unique<PartType>(This, std::forward<ArgsType>(Args)...));
508 }
509 void Init(CGameClient &This)
510 {
511 if(m_Inited)
512 return;
513 m_Inited = true;
514
515 AddPart<CNamePlatePartDirection>(This, Args: DIRECTION_LEFT);
516 AddPart<CNamePlatePartDirection>(This, Args: DIRECTION_UP);
517 AddPart<CNamePlatePartDirection>(This, Args: DIRECTION_RIGHT);
518 AddPart<CNamePlatePartNewLine>(This);
519
520 AddPart<CNamePlatePartFriendMark>(This);
521 AddPart<CNamePlatePartClientId>(This, Args: false);
522 AddPart<CNamePlatePartName>(This);
523 AddPart<CNamePlatePartNewLine>(This);
524
525 AddPart<CNamePlatePartClan>(This);
526 AddPart<CNamePlatePartNewLine>(This);
527
528 AddPart<CNamePlatePartClientId>(This, Args: true);
529 AddPart<CNamePlatePartNewLine>(This);
530
531 AddPart<CNamePlatePartHookStrongWeak>(This);
532 AddPart<CNamePlatePartHookStrongWeakId>(This);
533 }
534
535public:
536 CNamePlate() = default;
537 CNamePlate(CGameClient &This, const CNamePlateData &Data)
538 {
539 // Convenience constructor
540 Update(This, Data);
541 }
542 void Reset(CGameClient &This)
543 {
544 for(auto &Part : m_vpParts)
545 Part->Reset(This);
546 }
547 void Update(CGameClient &This, const CNamePlateData &Data)
548 {
549 Init(This);
550 m_InGame = Data.m_InGame;
551 for(auto &Part : m_vpParts)
552 Part->Update(This, Data);
553 }
554 void Render(CGameClient &This, const vec2 &PositionBottomMiddle)
555 {
556 dbg_assert(m_Inited, "Tried to render uninited nameplate");
557 vec2 Position = PositionBottomMiddle;
558 // X: Total width including padding of line, Y: Max height of line parts
559 vec2 LineSize = vec2(0.0f, 0.0f);
560 bool Empty = true;
561 auto Start = m_vpParts.begin();
562 for(auto PartIt = m_vpParts.begin(); PartIt != m_vpParts.end(); ++PartIt)
563 {
564 CNamePlatePart &Part = **PartIt;
565 if(Part.NewLine())
566 {
567 if(!Empty)
568 {
569 RenderLine(This, Pos: Position, Size: LineSize, Start, End: std::next(x: PartIt));
570 Position.y -= LineSize.y;
571 }
572 Start = std::next(x: PartIt);
573 LineSize = vec2(0.0f, 0.0f);
574 }
575 else if(Part.Visible() || Part.ShiftOnInvis())
576 {
577 Empty = false;
578 LineSize.x += Part.Size().x + Part.Padding().x;
579 LineSize.y = std::max(a: LineSize.y, b: Part.Size().y + Part.Padding().y);
580 }
581 }
582 RenderLine(This, Pos: Position, Size: LineSize, Start, End: m_vpParts.end());
583 This.Graphics()->SetColor(r: 1.0f, g: 1.0f, b: 1.0f, a: 1.0f);
584 }
585 vec2 Size() const
586 {
587 dbg_assert(m_Inited, "Tried to get size of uninited nameplate");
588 // X: Total width including padding of line, Y: Max height of line parts
589 vec2 LineSize = vec2(0.0f, 0.0f);
590 float WMax = 0.0f;
591 float HTotal = 0.0f;
592 bool Empty = true;
593 for(auto PartIt = m_vpParts.begin(); PartIt != m_vpParts.end(); ++PartIt) // NOLINT(modernize-loop-convert) For consistency with Render
594 {
595 CNamePlatePart &Part = **PartIt;
596 if(Part.NewLine())
597 {
598 if(!Empty)
599 {
600 if(LineSize.x > WMax)
601 WMax = LineSize.x;
602 HTotal += LineSize.y;
603 }
604 LineSize = vec2(0.0f, 0.0f);
605 }
606 else if(Part.Visible() || Part.ShiftOnInvis())
607 {
608 Empty = false;
609 LineSize.x += Part.Size().x + Part.Padding().x;
610 LineSize.y = std::max(a: LineSize.y, b: Part.Size().y + Part.Padding().y);
611 }
612 }
613 if(LineSize.x > WMax)
614 WMax = LineSize.x;
615 HTotal += LineSize.y;
616 return vec2(WMax, HTotal);
617 }
618};
619
620class CNamePlates::CNamePlatesData
621{
622public:
623 CNamePlate m_aNamePlates[MAX_CLIENTS];
624};
625
626void CNamePlates::RenderNamePlateGame(vec2 Position, const CNetObj_PlayerInfo *pPlayerInfo, float Alpha)
627{
628 // Get screen edges to avoid rendering offscreen
629 float ScreenX0, ScreenY0, ScreenX1, ScreenY1;
630 Graphics()->GetScreen(pTopLeftX: &ScreenX0, pTopLeftY: &ScreenY0, pBottomRightX: &ScreenX1, pBottomRightY: &ScreenY1);
631
632 // Assume that the name plate fits into a 800x800 box placed directly above the tee
633 ScreenX0 -= 400;
634 ScreenX1 += 400;
635 ScreenY1 += 800;
636 if(!(in_range(a: Position.x, lower: ScreenX0, upper: ScreenX1) && in_range(a: Position.y, lower: ScreenY0, upper: ScreenY1)))
637 return;
638
639 CNamePlateData Data;
640
641 const auto &ClientData = GameClient()->m_aClients[pPlayerInfo->m_ClientId];
642 const bool OtherTeam = GameClient()->IsOtherTeam(ClientId: pPlayerInfo->m_ClientId);
643
644 Data.m_InGame = true;
645
646 Data.m_ShowName = pPlayerInfo->m_Local ? g_Config.m_ClNamePlatesOwn : g_Config.m_ClNamePlates;
647 str_copy(dst&: Data.m_aName, src: GameClient()->m_aClients[pPlayerInfo->m_ClientId].m_aName);
648 Data.m_ShowFriendMark = Data.m_ShowName && g_Config.m_ClNamePlatesFriendMark && GameClient()->m_aClients[pPlayerInfo->m_ClientId].m_Friend;
649 Data.m_ShowClientId = Data.m_ShowName && (g_Config.m_Debug || g_Config.m_ClNamePlatesIds);
650 Data.m_FontSize = 18.0f + 20.0f * g_Config.m_ClNamePlatesSize / 100.0f;
651
652 Data.m_ClientId = pPlayerInfo->m_ClientId;
653 Data.m_ClientIdSeparateLine = g_Config.m_ClNamePlatesIdsSeparateLine;
654 Data.m_FontSizeClientId = Data.m_ClientIdSeparateLine ? (18.0f + 20.0f * g_Config.m_ClNamePlatesIdsSize / 100.0f) : Data.m_FontSize;
655
656 Data.m_ShowClan = Data.m_ShowName && g_Config.m_ClNamePlatesClan;
657 str_copy(dst&: Data.m_aClan, src: GameClient()->m_aClients[pPlayerInfo->m_ClientId].m_aClan);
658 Data.m_FontSizeClan = 18.0f + 20.0f * g_Config.m_ClNamePlatesClanSize / 100.0f;
659
660 Data.m_FontSizeHookStrongWeak = 18.0f + 20.0f * g_Config.m_ClNamePlatesStrongSize / 100.0f;
661 Data.m_FontSizeDirection = 18.0f + 20.0f * g_Config.m_ClDirectionSize / 100.0f;
662
663 if(g_Config.m_ClNamePlatesAlways == 0)
664 Alpha *= std::clamp(val: 1.0f - std::pow(x: distance(a: GameClient()->m_Controls.m_aTargetPos[g_Config.m_ClDummy], b: Position) / 200.0f, y: 16.0f), lo: 0.0f, hi: 1.0f);
665 if(OtherTeam)
666 Alpha *= (float)g_Config.m_ClShowOthersAlpha / 100.0f;
667
668 Data.m_Color = ColorRGBA(1.0f, 1.0f, 1.0f);
669 if(g_Config.m_ClNamePlatesTeamcolors)
670 {
671 if(GameClient()->IsTeamPlay())
672 {
673 if(ClientData.m_Team == TEAM_RED)
674 Data.m_Color = ColorRGBA(1.0f, 0.5f, 0.5f);
675 else if(ClientData.m_Team == TEAM_BLUE)
676 Data.m_Color = ColorRGBA(0.7f, 0.7f, 1.0f);
677 }
678 else
679 {
680 const int Team = GameClient()->m_Teams.Team(ClientId: pPlayerInfo->m_ClientId);
681 if(Team)
682 Data.m_Color = GameClient()->GetDDTeamColor(DDTeam: Team, Lightness: 0.75f);
683 }
684 }
685 Data.m_Color.a = Alpha;
686
687 int ShowDirectionConfig = g_Config.m_ClShowDirection;
688#if defined(CONF_VIDEORECORDER)
689 if(IVideo::Current())
690 ShowDirectionConfig = g_Config.m_ClVideoShowDirection;
691#endif
692 Data.m_DirLeft = Data.m_DirJump = Data.m_DirRight = false;
693 switch(ShowDirectionConfig)
694 {
695 case 0: // Off
696 Data.m_ShowDirection = false;
697 break;
698 case 1: // Others
699 Data.m_ShowDirection = !pPlayerInfo->m_Local;
700 break;
701 case 2: // Everyone
702 Data.m_ShowDirection = true;
703 break;
704 case 3: // Only self
705 Data.m_ShowDirection = pPlayerInfo->m_Local;
706 break;
707 default:
708 dbg_assert_failed("ShowDirectionConfig invalid");
709 }
710 if(Data.m_ShowDirection)
711 {
712 if(Client()->State() != IClient::STATE_DEMOPLAYBACK &&
713 pPlayerInfo->m_ClientId == GameClient()->m_aLocalIds[!g_Config.m_ClDummy])
714 {
715 const auto &InputData = GameClient()->m_Controls.m_aInputData[!g_Config.m_ClDummy];
716 Data.m_DirLeft = InputData.m_Direction == -1;
717 Data.m_DirJump = InputData.m_Jump == 1;
718 Data.m_DirRight = InputData.m_Direction == 1;
719 }
720 else if(Client()->State() != IClient::STATE_DEMOPLAYBACK && pPlayerInfo->m_Local) // Always render local input when not in demo playback
721 {
722 const auto &InputData = GameClient()->m_Controls.m_aInputData[g_Config.m_ClDummy];
723 Data.m_DirLeft = InputData.m_Direction == -1;
724 Data.m_DirJump = InputData.m_Jump == 1;
725 Data.m_DirRight = InputData.m_Direction == 1;
726 }
727 else
728 {
729 const auto &Character = GameClient()->m_Snap.m_aCharacters[pPlayerInfo->m_ClientId];
730 Data.m_DirLeft = Character.m_Cur.m_Direction == -1;
731 Data.m_DirJump = Character.m_Cur.m_Jumped & 1;
732 Data.m_DirRight = Character.m_Cur.m_Direction == 1;
733 }
734 }
735
736 Data.m_ShowHookStrongWeak = false;
737 Data.m_HookStrongWeakState = EHookStrongWeakState::NEUTRAL;
738 Data.m_ShowHookStrongWeakId = false;
739 Data.m_HookStrongWeakId = 0;
740
741 const bool Following = (GameClient()->m_Snap.m_SpecInfo.m_Active && !GameClient()->m_MultiViewActivated && GameClient()->m_Snap.m_SpecInfo.m_SpectatorId != SPEC_FREEVIEW);
742 if(GameClient()->m_Snap.m_LocalClientId != -1 || Following)
743 {
744 const int SelectedId = Following ? GameClient()->m_Snap.m_SpecInfo.m_SpectatorId : GameClient()->m_Snap.m_LocalClientId;
745 const CGameClient::CSnapState::CCharacterInfo &Selected = GameClient()->m_Snap.m_aCharacters[SelectedId];
746 const CGameClient::CSnapState::CCharacterInfo &Other = GameClient()->m_Snap.m_aCharacters[pPlayerInfo->m_ClientId];
747
748 if((Selected.m_HasExtendedData || GameClient()->m_aClients[SelectedId].m_SpecCharPresent) && Other.m_HasExtendedData)
749 {
750 int SelectedStrongWeakId = Selected.m_HasExtendedData ? Selected.m_ExtendedData.m_StrongWeakId : 0;
751 Data.m_HookStrongWeakId = Other.m_ExtendedData.m_StrongWeakId;
752 Data.m_ShowHookStrongWeakId = g_Config.m_Debug || g_Config.m_ClNamePlatesStrong == 2;
753 if(SelectedId == pPlayerInfo->m_ClientId)
754 Data.m_ShowHookStrongWeak = Data.m_ShowHookStrongWeakId;
755 else
756 {
757 Data.m_HookStrongWeakState = SelectedStrongWeakId > Other.m_ExtendedData.m_StrongWeakId ? EHookStrongWeakState::STRONG : EHookStrongWeakState::WEAK;
758 Data.m_ShowHookStrongWeak = g_Config.m_Debug || g_Config.m_ClNamePlatesStrong > 0;
759 }
760 }
761 }
762
763 // Check if the nameplate is actually on screen
764 CNamePlate &NamePlate = m_pData->m_aNamePlates[pPlayerInfo->m_ClientId];
765 NamePlate.Update(This&: *GameClient(), Data);
766 NamePlate.Render(This&: *GameClient(), PositionBottomMiddle: Position - vec2(0.0f, (float)g_Config.m_ClNamePlatesOffset));
767}
768
769void CNamePlates::RenderNamePlatePreview(vec2 Position, int Dummy)
770{
771 const float FontSize = 18.0f + 20.0f * g_Config.m_ClNamePlatesSize / 100.0f;
772 const float FontSizeClan = 18.0f + 20.0f * g_Config.m_ClNamePlatesClanSize / 100.0f;
773
774 const float FontSizeDirection = 18.0f + 20.0f * g_Config.m_ClDirectionSize / 100.0f;
775 const float FontSizeHookStrongWeak = 18.0f + 20.0f * g_Config.m_ClNamePlatesStrongSize / 100.0f;
776
777 CNamePlateData Data;
778
779 Data.m_InGame = false;
780 Data.m_Color = g_Config.m_ClNamePlatesTeamcolors ? GameClient()->GetDDTeamColor(DDTeam: 13, Lightness: 0.75f) : TextRender()->DefaultTextColor();
781 Data.m_Color.a = 1.0f;
782
783 Data.m_ShowName = g_Config.m_ClNamePlates || g_Config.m_ClNamePlatesOwn;
784 const char *pName = Dummy == 0 ? Client()->PlayerName() : Client()->DummyName();
785 str_copy(dst&: Data.m_aName, src: str_utf8_skip_whitespaces(str: pName));
786 str_utf8_trim_right(param: Data.m_aName);
787 Data.m_FontSize = FontSize;
788
789 Data.m_ShowFriendMark = Data.m_ShowName && g_Config.m_ClNamePlatesFriendMark;
790
791 Data.m_ShowClientId = Data.m_ShowName && (g_Config.m_Debug || g_Config.m_ClNamePlatesIds);
792 Data.m_ClientId = Dummy;
793 Data.m_ClientIdSeparateLine = g_Config.m_ClNamePlatesIdsSeparateLine;
794 Data.m_FontSizeClientId = Data.m_ClientIdSeparateLine ? (18.0f + 20.0f * g_Config.m_ClNamePlatesIdsSize / 100.0f) : Data.m_FontSize;
795
796 Data.m_ShowClan = Data.m_ShowName && g_Config.m_ClNamePlatesClan;
797 const char *pClan = Dummy == 0 ? g_Config.m_PlayerClan : g_Config.m_ClDummyClan;
798 str_copy(dst&: Data.m_aClan, src: str_utf8_skip_whitespaces(str: pClan));
799 str_utf8_trim_right(param: Data.m_aClan);
800 if(Data.m_aClan[0] == '\0')
801 str_copy(dst&: Data.m_aClan, src: "Clan Name");
802 Data.m_FontSizeClan = FontSizeClan;
803
804 Data.m_ShowDirection = g_Config.m_ClShowDirection != 0 ? true : false;
805 Data.m_DirLeft = Data.m_DirJump = Data.m_DirRight = true;
806 Data.m_FontSizeDirection = FontSizeDirection;
807
808 Data.m_FontSizeHookStrongWeak = FontSizeHookStrongWeak;
809 Data.m_HookStrongWeakId = Data.m_ClientId;
810 Data.m_ShowHookStrongWeakId = g_Config.m_ClNamePlatesStrong == 2;
811 if(Dummy == g_Config.m_ClDummy)
812 {
813 Data.m_HookStrongWeakState = EHookStrongWeakState::NEUTRAL;
814 Data.m_ShowHookStrongWeak = Data.m_ShowHookStrongWeakId;
815 }
816 else
817 {
818 Data.m_HookStrongWeakState = Data.m_HookStrongWeakId == 2 ? EHookStrongWeakState::STRONG : EHookStrongWeakState::WEAK;
819 Data.m_ShowHookStrongWeak = g_Config.m_ClNamePlatesStrong > 0;
820 }
821
822 CTeeRenderInfo TeeRenderInfo;
823 if(Dummy == 0)
824 {
825 TeeRenderInfo.Apply(pSkin: GameClient()->m_Skins.Find(pName: g_Config.m_ClPlayerSkin));
826 TeeRenderInfo.ApplyColors(CustomColoredSkin: g_Config.m_ClPlayerUseCustomColor, ColorBody: g_Config.m_ClPlayerColorBody, ColorFeet: g_Config.m_ClPlayerColorFeet);
827 }
828 else
829 {
830 TeeRenderInfo.Apply(pSkin: GameClient()->m_Skins.Find(pName: g_Config.m_ClDummySkin));
831 TeeRenderInfo.ApplyColors(CustomColoredSkin: g_Config.m_ClDummyUseCustomColor, ColorBody: g_Config.m_ClDummyColorBody, ColorFeet: g_Config.m_ClDummyColorFeet);
832 }
833 TeeRenderInfo.m_Size = 64.0f;
834
835 CNamePlate NamePlate(*GameClient(), Data);
836 Position.y += NamePlate.Size().y / 2.0f;
837 Position.y += (float)g_Config.m_ClNamePlatesOffset / 2.0f;
838 // tee looking towards cursor, and it is happy when you touch it
839 const vec2 DeltaPosition = Ui()->MousePos() - Position;
840 const float Distance = length(a: DeltaPosition);
841 const float InteractionDistance = 20.0f;
842 const vec2 TeeDirection = Distance < InteractionDistance ? normalize(v: vec2(DeltaPosition.x, maximum(a: DeltaPosition.y, b: 0.5f))) : normalize(v: DeltaPosition);
843 const int TeeEmote = Distance < InteractionDistance ? EMOTE_HAPPY : (Dummy ? g_Config.m_ClDummyDefaultEyes : g_Config.m_ClPlayerDefaultEyes);
844 RenderTools()->RenderTee(pAnim: CAnimState::GetIdle(), pInfo: &TeeRenderInfo, Emote: TeeEmote, Dir: TeeDirection, Pos: Position);
845 Position.y -= (float)g_Config.m_ClNamePlatesOffset;
846 NamePlate.Render(This&: *GameClient(), PositionBottomMiddle: Position);
847 NamePlate.Reset(This&: *GameClient());
848}
849
850void CNamePlates::ResetNamePlates()
851{
852 for(CNamePlate &NamePlate : m_pData->m_aNamePlates)
853 NamePlate.Reset(This&: *GameClient());
854}
855
856void CNamePlates::OnRender()
857{
858 if(Client()->State() != IClient::STATE_ONLINE && Client()->State() != IClient::STATE_DEMOPLAYBACK)
859 return;
860
861 int ShowDirection = g_Config.m_ClShowDirection;
862#if defined(CONF_VIDEORECORDER)
863 if(IVideo::Current())
864 ShowDirection = g_Config.m_ClVideoShowDirection;
865#endif
866 if(!g_Config.m_ClNamePlates && ShowDirection == 0)
867 return;
868
869 for(int i = 0; i < MAX_CLIENTS; i++)
870 {
871 const CNetObj_PlayerInfo *pInfo = GameClient()->m_Snap.m_apPlayerInfos[i];
872 if(!pInfo)
873 continue;
874
875 // Each player can also have a spectator char whose name plate is displayed independently
876 if(GameClient()->m_aClients[i].m_SpecCharPresent)
877 {
878 const vec2 RenderPos = GameClient()->m_aClients[i].m_SpecChar;
879 RenderNamePlateGame(Position: RenderPos, pPlayerInfo: pInfo, Alpha: 0.4f);
880 }
881 // Only render name plates for active characters
882 if(GameClient()->m_Snap.m_aCharacters[i].m_Active)
883 {
884 const vec2 RenderPos = GameClient()->m_aClients[i].m_RenderPos;
885 RenderNamePlateGame(Position: RenderPos, pPlayerInfo: pInfo, Alpha: 1.0f);
886 }
887 }
888}
889
890void CNamePlates::OnWindowResize()
891{
892 ResetNamePlates();
893}
894
895CNamePlates::CNamePlates() :
896 m_pData(new CNamePlates::CNamePlatesData()) {}
897
898CNamePlates::~CNamePlates()
899{
900 delete m_pData;
901}
902