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