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(a: (size_t)MAX_NAME_LENGTH, b: (size_t)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(a: (size_t)MAX_CLAN_LENGTH, b: (size_t)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 CScreenRect ScreenRect = This.Graphics()->GetScreen();
111 This.Graphics()->MapScreenToInterface(CenterX: This.m_Camera.m_Center.x, CenterY: This.m_Camera.m_Center.y);
112 This.TextRender()->DeleteTextContainer(TextContainerIndex&: m_TextContainerIndex);
113 UpdateText(This, Data);
114 This.Graphics()->MapScreen(ScreenRect);
115 }
116 else
117 {
118 UpdateText(This, Data);
119 }
120
121 This.TextRender()->SetRenderFlags(0);
122
123 if(!m_TextContainerIndex.Valid())
124 {
125 m_Visible = false;
126 return;
127 }
128
129 const STextBoundingBox Container = This.TextRender()->GetBoundingBoxTextContainer(TextContainerIndex: m_TextContainerIndex);
130 m_Size = vec2(Container.m_W, Container.m_H);
131 }
132 void Reset(CGameClient &This) override
133 {
134 This.TextRender()->DeleteTextContainer(TextContainerIndex&: m_TextContainerIndex);
135 }
136 void Render(CGameClient &This, vec2 Pos) const override
137 {
138 if(!m_TextContainerIndex.Valid())
139 return;
140
141 ColorRGBA OutlineColor, Color;
142 Color = m_Color;
143 OutlineColor = ColorRGBA(0.0f, 0.0f, 0.0f, 0.5f * m_Color.a);
144 This.TextRender()->RenderTextContainer(TextContainerIndex: m_TextContainerIndex,
145 TextColor: Color, TextOutlineColor: OutlineColor,
146 X: Pos.x - Size().x / 2.0f, Y: Pos.y - Size().y / 2.0f);
147 }
148};
149
150class CNamePlatePartIcon : public CNamePlatePart
151{
152protected:
153 IGraphics::CTextureHandle m_Texture;
154 float m_Rotation = 0.0f;
155 ColorRGBA m_Color = ColorRGBA(1.0f, 1.0f, 1.0f, 1.0f);
156 CNamePlatePartIcon(CGameClient &This) :
157 CNamePlatePart(This) {}
158
159public:
160 void Render(CGameClient &This, vec2 Pos) const override
161 {
162 IGraphics::CQuadItem QuadItem(Pos.x - Size().x / 2.0f, Pos.y - Size().y / 2.0f, Size().x, Size().y);
163 This.Graphics()->TextureSet(Texture: m_Texture);
164 This.Graphics()->QuadsBegin();
165 This.Graphics()->SetColor(m_Color);
166 This.Graphics()->QuadsSetRotation(Angle: m_Rotation);
167 This.Graphics()->QuadsDrawTL(pArray: &QuadItem, Num: 1);
168 This.Graphics()->QuadsEnd();
169 This.Graphics()->QuadsSetRotation(Angle: 0.0f);
170 }
171};
172
173class CNamePlatePartSprite : public CNamePlatePart
174{
175protected:
176 IGraphics::CTextureHandle m_Texture;
177 int m_Sprite = -1;
178 int m_SpriteFlags = 0;
179 float m_Rotation = 0.0f;
180 ColorRGBA m_Color = ColorRGBA(1.0f, 1.0f, 1.0f, 1.0f);
181 CNamePlatePartSprite(CGameClient &This) :
182 CNamePlatePart(This) {}
183
184public:
185 void Render(CGameClient &This, vec2 Pos) const override
186 {
187 This.Graphics()->TextureSet(Texture: m_Texture);
188 This.Graphics()->QuadsSetRotation(Angle: m_Rotation);
189 This.Graphics()->QuadsBegin();
190 This.Graphics()->SetColor(m_Color);
191 This.Graphics()->SelectSprite(Id: m_Sprite, Flags: m_SpriteFlags);
192 This.Graphics()->DrawSprite(x: Pos.x, y: Pos.y, ScaledWidth: Size().x, ScaledHeight: Size().y);
193 This.Graphics()->QuadsEnd();
194 This.Graphics()->QuadsSetRotation(Angle: 0.0f);
195 }
196};
197
198// Part Definitions
199
200class CNamePlatePartNewLine : public CNamePlatePart
201{
202public:
203 CNamePlatePartNewLine(CGameClient &This) :
204 CNamePlatePart(This)
205 {
206 m_NewLine = true;
207 }
208};
209
210enum Direction
211{
212 DIRECTION_LEFT,
213 DIRECTION_UP,
214 DIRECTION_RIGHT
215};
216
217class CNamePlatePartDirection : public CNamePlatePartIcon
218{
219private:
220 int m_Direction;
221
222public:
223 CNamePlatePartDirection(CGameClient &This, Direction Dir) :
224 CNamePlatePartIcon(This)
225 {
226 m_Texture = g_pData->m_aImages[IMAGE_ARROW].m_Id;
227 m_Direction = Dir;
228 switch(m_Direction)
229 {
230 case DIRECTION_LEFT:
231 m_Rotation = pi;
232 break;
233 case DIRECTION_UP:
234 m_Rotation = pi / -2.0f;
235 break;
236 case DIRECTION_RIGHT:
237 m_Rotation = 0.0f;
238 break;
239 }
240 }
241 void Update(CGameClient &This, const CNamePlateData &Data) override
242 {
243 if(!Data.m_ShowDirection)
244 {
245 m_ShiftOnInvis = false;
246 m_Visible = false;
247 return;
248 }
249 m_ShiftOnInvis = true; // Only shift (horizontally) the other parts if directions as a whole is visible
250 m_Size = vec2(Data.m_FontSizeDirection, Data.m_FontSizeDirection);
251 m_Padding.y = m_Size.y / 2.0f;
252 switch(m_Direction)
253 {
254 case DIRECTION_LEFT:
255 m_Visible = Data.m_DirLeft;
256 break;
257 case DIRECTION_UP:
258 m_Visible = Data.m_DirJump;
259 break;
260 case DIRECTION_RIGHT:
261 m_Visible = Data.m_DirRight;
262 break;
263 }
264 m_Color.a = Data.m_Color.a;
265 }
266};
267
268class CNamePlatePartClientId : public CNamePlatePartText
269{
270private:
271 int m_ClientId = -1;
272 static_assert(MAX_CLIENTS <= 999, "Make this buffer bigger");
273 char m_aText[5] = "";
274 float m_FontSize = -INFINITY;
275 bool m_ClientIdSeparateLine = false;
276
277protected:
278 bool UpdateNeeded(CGameClient &This, const CNamePlateData &Data) override
279 {
280 m_Visible = Data.m_ShowClientId && (Data.m_ClientIdSeparateLine == m_ClientIdSeparateLine);
281 if(!m_Visible)
282 return false;
283 m_Color = Data.m_Color;
284 return m_FontSize != Data.m_FontSizeClientId || m_ClientId != Data.m_ClientId;
285 }
286 void UpdateText(CGameClient &This, const CNamePlateData &Data) override
287 {
288 m_FontSize = Data.m_FontSizeClientId;
289 m_ClientId = Data.m_ClientId;
290 if(m_ClientIdSeparateLine)
291 str_format(buffer: m_aText, buffer_size: sizeof(m_aText), format: "%d", m_ClientId);
292 else
293 str_format(buffer: m_aText, buffer_size: sizeof(m_aText), format: "%d:", m_ClientId);
294 CTextCursor Cursor;
295 Cursor.m_FontSize = m_FontSize;
296 This.TextRender()->CreateOrAppendTextContainer(TextContainerIndex&: m_TextContainerIndex, pCursor: &Cursor, pText: m_aText);
297 }
298
299public:
300 CNamePlatePartClientId(CGameClient &This, bool ClientIdSeparateLine) :
301 CNamePlatePartText(This)
302 {
303 m_ClientIdSeparateLine = ClientIdSeparateLine;
304 }
305};
306
307class CNamePlatePartFriendMark : public CNamePlatePartText
308{
309private:
310 float m_FontSize = -INFINITY;
311
312protected:
313 bool UpdateNeeded(CGameClient &This, const CNamePlateData &Data) override
314 {
315 m_Visible = Data.m_ShowFriendMark;
316 if(!m_Visible)
317 return false;
318 m_Color.a = Data.m_Color.a;
319 return m_FontSize != Data.m_FontSize;
320 }
321 void UpdateText(CGameClient &This, const CNamePlateData &Data) override
322 {
323 m_FontSize = Data.m_FontSize;
324 CTextCursor Cursor;
325 This.TextRender()->SetFontPreset(EFontPreset::ICON_FONT);
326 Cursor.m_FontSize = m_FontSize;
327 This.TextRender()->CreateOrAppendTextContainer(TextContainerIndex&: m_TextContainerIndex, pCursor: &Cursor, pText: FontIcon::HEART);
328 This.TextRender()->SetFontPreset(EFontPreset::DEFAULT_FONT);
329 }
330
331public:
332 CNamePlatePartFriendMark(CGameClient &This) :
333 CNamePlatePartText(This)
334 {
335 m_Color = ColorRGBA(1.0f, 0.0f, 0.0f);
336 }
337};
338
339class CNamePlatePartName : public CNamePlatePartText
340{
341private:
342 char m_aText[std::max(a: (size_t)MAX_NAME_LENGTH, b: (size_t)protocol7::MAX_NAME_ARRAY_SIZE)] = "";
343 float m_FontSize = -INFINITY;
344
345protected:
346 bool UpdateNeeded(CGameClient &This, const CNamePlateData &Data) override
347 {
348 m_Visible = Data.m_ShowName;
349 if(!m_Visible)
350 return false;
351 m_Color = Data.m_Color;
352 return m_FontSize != Data.m_FontSize || str_comp(a: m_aText, b: Data.m_aName) != 0;
353 }
354 void UpdateText(CGameClient &This, const CNamePlateData &Data) override
355 {
356 m_FontSize = Data.m_FontSize;
357 str_copy(dst&: m_aText, src: Data.m_aName);
358 CTextCursor Cursor;
359 Cursor.m_FontSize = m_FontSize;
360 This.TextRender()->CreateOrAppendTextContainer(TextContainerIndex&: m_TextContainerIndex, pCursor: &Cursor, pText: m_aText);
361 }
362
363public:
364 CNamePlatePartName(CGameClient &This) :
365 CNamePlatePartText(This) {}
366};
367
368class CNamePlatePartClan : public CNamePlatePartText
369{
370private:
371 char m_aText[std::max(a: (size_t)MAX_CLAN_LENGTH, b: (size_t)protocol7::MAX_CLAN_ARRAY_SIZE)] = "";
372 float m_FontSize = -INFINITY;
373
374protected:
375 bool UpdateNeeded(CGameClient &This, const CNamePlateData &Data) override
376 {
377 m_Visible = Data.m_ShowClan;
378 if(!m_Visible && Data.m_aClan[0] != '\0')
379 return false;
380 m_Color = Data.m_Color;
381 return m_FontSize != Data.m_FontSizeClan || str_comp(a: m_aText, b: Data.m_aClan) != 0;
382 }
383 void UpdateText(CGameClient &This, const CNamePlateData &Data) override
384 {
385 m_FontSize = Data.m_FontSizeClan;
386 str_copy(dst&: m_aText, src: Data.m_aClan);
387 CTextCursor Cursor;
388 Cursor.m_FontSize = m_FontSize;
389 This.TextRender()->CreateOrAppendTextContainer(TextContainerIndex&: m_TextContainerIndex, pCursor: &Cursor, pText: m_aText);
390 }
391
392public:
393 CNamePlatePartClan(CGameClient &This) :
394 CNamePlatePartText(This) {}
395};
396
397class CNamePlatePartHookStrongWeak : public CNamePlatePartSprite
398{
399protected:
400 void Update(CGameClient &This, const CNamePlateData &Data) override
401 {
402 m_Visible = Data.m_ShowHookStrongWeak;
403 if(!m_Visible)
404 return;
405 m_Size = vec2(Data.m_FontSizeHookStrongWeak + DEFAULT_PADDING, Data.m_FontSizeHookStrongWeak + DEFAULT_PADDING);
406 switch(Data.m_HookStrongWeakState)
407 {
408 case EHookStrongWeakState::STRONG:
409 m_Sprite = SPRITE_HOOK_STRONG;
410 m_Color = color_cast<ColorRGBA>(hsl: ColorHSLA(6401973));
411 break;
412 case EHookStrongWeakState::NEUTRAL:
413 m_Sprite = SPRITE_HOOK_ICON;
414 m_Color = ColorRGBA(1.0f, 1.0f, 1.0f);
415 break;
416 case EHookStrongWeakState::WEAK:
417 m_Sprite = SPRITE_HOOK_WEAK;
418 m_Color = color_cast<ColorRGBA>(hsl: ColorHSLA(41131));
419 break;
420 }
421 m_Color.a = Data.m_Color.a;
422 }
423
424public:
425 CNamePlatePartHookStrongWeak(CGameClient &This) :
426 CNamePlatePartSprite(This)
427 {
428 m_Texture = g_pData->m_aImages[IMAGE_STRONGWEAK].m_Id;
429 m_Padding = vec2(0.0f, 0.0f);
430 }
431};
432
433class CNamePlatePartHookStrongWeakId : public CNamePlatePartText
434{
435private:
436 int m_StrongWeakId = -1;
437 static_assert(MAX_CLIENTS <= 999, "Make this buffer bigger");
438 char m_aText[4] = "";
439 float m_FontSize = -INFINITY;
440
441protected:
442 bool UpdateNeeded(CGameClient &This, const CNamePlateData &Data) override
443 {
444 m_Visible = Data.m_ShowHookStrongWeakId;
445 if(!m_Visible)
446 return false;
447 switch(Data.m_HookStrongWeakState)
448 {
449 case EHookStrongWeakState::STRONG:
450 m_Color = color_cast<ColorRGBA>(hsl: ColorHSLA(6401973));
451 break;
452 case EHookStrongWeakState::NEUTRAL:
453 m_Color = ColorRGBA(1.0f, 1.0f, 1.0f);
454 break;
455 case EHookStrongWeakState::WEAK:
456 m_Color = color_cast<ColorRGBA>(hsl: ColorHSLA(41131));
457 break;
458 }
459 m_Color.a = Data.m_Color.a;
460 return m_FontSize != Data.m_FontSizeHookStrongWeak || m_StrongWeakId != Data.m_HookStrongWeakId;
461 }
462 void UpdateText(CGameClient &This, const CNamePlateData &Data) override
463 {
464 m_FontSize = Data.m_FontSizeHookStrongWeak;
465 m_StrongWeakId = Data.m_HookStrongWeakId;
466 str_format(buffer: m_aText, buffer_size: sizeof(m_aText), format: "%d", m_StrongWeakId);
467 CTextCursor Cursor;
468 Cursor.m_FontSize = m_FontSize;
469 This.TextRender()->CreateOrAppendTextContainer(TextContainerIndex&: m_TextContainerIndex, pCursor: &Cursor, pText: m_aText);
470 }
471
472public:
473 CNamePlatePartHookStrongWeakId(CGameClient &This) :
474 CNamePlatePartText(This) {}
475};
476
477// Name Plates
478
479class CNamePlate
480{
481private:
482 bool m_Inited = false;
483 bool m_InGame = false;
484 PartsVector m_vpParts;
485 void RenderLine(CGameClient &This,
486 vec2 Pos, vec2 Size,
487 const PartsVector::iterator &Start, const PartsVector::iterator &End)
488 {
489 Pos.x -= Size.x / 2.0f;
490 for(auto PartIt = Start; PartIt != End; ++PartIt)
491 {
492 const CNamePlatePart &Part = **PartIt;
493 if(Part.Visible())
494 {
495 Part.Render(This, Pos: vec2(
496 Pos.x + (Part.Padding().x + Part.Size().x) / 2.0f,
497 Pos.y - std::max(a: Size.y, b: Part.Padding().y + Part.Size().y) / 2.0f));
498 }
499 if(Part.Visible() || Part.ShiftOnInvis())
500 Pos.x += Part.Size().x + Part.Padding().x;
501 }
502 }
503 template<typename PartType, typename... ArgsType>
504 void AddPart(CGameClient &This, ArgsType &&...Args)
505 {
506 m_vpParts.push_back(std::make_unique<PartType>(This, std::forward<ArgsType>(Args)...));
507 }
508 void Init(CGameClient &This)
509 {
510 if(m_Inited)
511 return;
512 m_Inited = true;
513
514 AddPart<CNamePlatePartDirection>(This, Args: DIRECTION_LEFT);
515 AddPart<CNamePlatePartDirection>(This, Args: DIRECTION_UP);
516 AddPart<CNamePlatePartDirection>(This, Args: DIRECTION_RIGHT);
517 AddPart<CNamePlatePartNewLine>(This);
518
519 AddPart<CNamePlatePartFriendMark>(This);
520 AddPart<CNamePlatePartClientId>(This, Args: false);
521 AddPart<CNamePlatePartName>(This);
522 AddPart<CNamePlatePartNewLine>(This);
523
524 AddPart<CNamePlatePartClan>(This);
525 AddPart<CNamePlatePartNewLine>(This);
526
527 AddPart<CNamePlatePartClientId>(This, Args: true);
528 AddPart<CNamePlatePartNewLine>(This);
529
530 AddPart<CNamePlatePartHookStrongWeak>(This);
531 AddPart<CNamePlatePartHookStrongWeakId>(This);
532 }
533
534public:
535 CNamePlate() = default;
536 CNamePlate(CGameClient &This, const CNamePlateData &Data)
537 {
538 // Convenience constructor
539 Update(This, Data);
540 }
541 void Reset(CGameClient &This)
542 {
543 for(auto &Part : m_vpParts)
544 Part->Reset(This);
545 }
546 void Update(CGameClient &This, const CNamePlateData &Data)
547 {
548 Init(This);
549 m_InGame = Data.m_InGame;
550 for(auto &Part : m_vpParts)
551 Part->Update(This, Data);
552 }
553 void Render(CGameClient &This, const vec2 &PositionBottomMiddle)
554 {
555 dbg_assert(m_Inited, "Tried to render uninited nameplate");
556 vec2 Position = PositionBottomMiddle;
557 // X: Total width including padding of line, Y: Max height of line parts
558 vec2 LineSize = vec2(0.0f, 0.0f);
559 bool Empty = true;
560 auto Start = m_vpParts.begin();
561 for(auto PartIt = m_vpParts.begin(); PartIt != m_vpParts.end(); ++PartIt)
562 {
563 CNamePlatePart &Part = **PartIt;
564 if(Part.NewLine())
565 {
566 if(!Empty)
567 {
568 RenderLine(This, Pos: Position, Size: LineSize, Start, End: std::next(x: PartIt));
569 Position.y -= LineSize.y;
570 }
571 Start = std::next(x: PartIt);
572 LineSize = vec2(0.0f, 0.0f);
573 }
574 else if(Part.Visible() || Part.ShiftOnInvis())
575 {
576 Empty = false;
577 LineSize.x += Part.Size().x + Part.Padding().x;
578 LineSize.y = std::max(a: LineSize.y, b: Part.Size().y + Part.Padding().y);
579 }
580 }
581 RenderLine(This, Pos: Position, Size: LineSize, Start, End: m_vpParts.end());
582 This.Graphics()->SetColor(r: 1.0f, g: 1.0f, b: 1.0f, a: 1.0f);
583 }
584 vec2 Size() const
585 {
586 dbg_assert(m_Inited, "Tried to get size of uninited nameplate");
587 // X: Total width including padding of line, Y: Max height of line parts
588 vec2 LineSize = vec2(0.0f, 0.0f);
589 float WMax = 0.0f;
590 float HTotal = 0.0f;
591 bool Empty = true;
592 for(auto PartIt = m_vpParts.begin(); PartIt != m_vpParts.end(); ++PartIt) // NOLINT(modernize-loop-convert) For consistency with Render
593 {
594 CNamePlatePart &Part = **PartIt;
595 if(Part.NewLine())
596 {
597 if(!Empty)
598 {
599 if(LineSize.x > WMax)
600 WMax = LineSize.x;
601 HTotal += LineSize.y;
602 }
603 LineSize = vec2(0.0f, 0.0f);
604 }
605 else if(Part.Visible() || Part.ShiftOnInvis())
606 {
607 Empty = false;
608 LineSize.x += Part.Size().x + Part.Padding().x;
609 LineSize.y = std::max(a: LineSize.y, b: Part.Size().y + Part.Padding().y);
610 }
611 }
612 if(LineSize.x > WMax)
613 WMax = LineSize.x;
614 HTotal += LineSize.y;
615 return vec2(WMax, HTotal);
616 }
617};
618
619class CNamePlates::CNamePlatesData
620{
621public:
622 CNamePlate m_aNamePlates[MAX_CLIENTS];
623};
624
625void CNamePlates::RenderNamePlateGame(vec2 Position, const CNetObj_PlayerInfo *pPlayerInfo, float Alpha)
626{
627 // Get screen edges to avoid rendering offscreen
628 CScreenRect ScreenRect = Graphics()->GetScreen();
629
630 // Assume that the name plate fits into a 800x800 box placed directly above the tee
631 ScreenRect.m_TopLeft.x -= 400;
632 ScreenRect.m_BottomRight.x += 400;
633 ScreenRect.m_BottomRight.y += 800;
634 if(!ScreenRect.Inside(Position))
635 return;
636
637 CNamePlateData Data;
638
639 const auto &ClientData = GameClient()->m_aClients[pPlayerInfo->m_ClientId];
640 const bool OtherTeam = GameClient()->IsOtherTeam(ClientId: pPlayerInfo->m_ClientId);
641
642 Data.m_InGame = true;
643
644 Data.m_ShowName = pPlayerInfo->m_Local ? g_Config.m_ClNamePlatesOwn : g_Config.m_ClNamePlates;
645 str_copy(dst&: Data.m_aName, src: GameClient()->m_aClients[pPlayerInfo->m_ClientId].m_aName);
646 Data.m_ShowFriendMark = Data.m_ShowName && g_Config.m_ClNamePlatesFriendMark && GameClient()->m_aClients[pPlayerInfo->m_ClientId].m_Friend;
647 Data.m_ShowClientId = Data.m_ShowName && (g_Config.m_Debug || g_Config.m_ClNamePlatesIds);
648 Data.m_FontSize = 18.0f + 20.0f * g_Config.m_ClNamePlatesSize / 100.0f;
649
650 Data.m_ClientId = pPlayerInfo->m_ClientId;
651 Data.m_ClientIdSeparateLine = g_Config.m_ClNamePlatesIdsSeparateLine;
652 Data.m_FontSizeClientId = Data.m_ClientIdSeparateLine ? (18.0f + 20.0f * g_Config.m_ClNamePlatesIdsSize / 100.0f) : Data.m_FontSize;
653
654 Data.m_ShowClan = Data.m_ShowName && g_Config.m_ClNamePlatesClan;
655 str_copy(dst&: Data.m_aClan, src: GameClient()->m_aClients[pPlayerInfo->m_ClientId].m_aClan);
656 Data.m_FontSizeClan = 18.0f + 20.0f * g_Config.m_ClNamePlatesClanSize / 100.0f;
657
658 Data.m_FontSizeHookStrongWeak = 18.0f + 20.0f * g_Config.m_ClNamePlatesStrongSize / 100.0f;
659 Data.m_FontSizeDirection = 18.0f + 20.0f * g_Config.m_ClDirectionSize / 100.0f;
660
661 if(g_Config.m_ClNamePlatesAlways == 0)
662 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);
663 if(OtherTeam)
664 Alpha *= (float)g_Config.m_ClShowOthersAlpha / 100.0f;
665
666 Data.m_Color = ColorRGBA(1.0f, 1.0f, 1.0f);
667 if(g_Config.m_ClNamePlatesTeamcolors)
668 {
669 if(GameClient()->IsTeamPlay())
670 {
671 if(ClientData.m_Team == TEAM_RED)
672 Data.m_Color = ColorRGBA(1.0f, 0.5f, 0.5f);
673 else if(ClientData.m_Team == TEAM_BLUE)
674 Data.m_Color = ColorRGBA(0.7f, 0.7f, 1.0f);
675 }
676 else
677 {
678 const int Team = GameClient()->m_Teams.Team(ClientId: pPlayerInfo->m_ClientId);
679 if(Team)
680 Data.m_Color = GameClient()->GetDDTeamColor(DDTeam: Team, Lightness: 0.75f);
681 }
682 }
683 Data.m_Color.a = Alpha;
684
685 int ShowDirectionConfig = g_Config.m_ClShowDirection;
686#if defined(CONF_VIDEORECORDER)
687 if(IVideo::Current())
688 ShowDirectionConfig = g_Config.m_ClVideoShowDirection;
689#endif
690 Data.m_DirLeft = Data.m_DirJump = Data.m_DirRight = false;
691 switch(ShowDirectionConfig)
692 {
693 case 0: // Off
694 Data.m_ShowDirection = false;
695 break;
696 case 1: // Others
697 Data.m_ShowDirection = !pPlayerInfo->m_Local;
698 break;
699 case 2: // Everyone
700 Data.m_ShowDirection = true;
701 break;
702 case 3: // Only self
703 Data.m_ShowDirection = pPlayerInfo->m_Local;
704 break;
705 default:
706 dbg_assert_failed("ShowDirectionConfig invalid");
707 }
708 if(Data.m_ShowDirection)
709 {
710 if(Client()->State() != IClient::STATE_DEMOPLAYBACK &&
711 pPlayerInfo->m_ClientId == GameClient()->m_aLocalIds[!g_Config.m_ClDummy])
712 {
713 const auto &InputData = GameClient()->m_Controls.m_aInputData[!g_Config.m_ClDummy];
714 Data.m_DirLeft = InputData.m_Direction == -1;
715 Data.m_DirJump = InputData.m_Jump == 1;
716 Data.m_DirRight = InputData.m_Direction == 1;
717 }
718 else if(Client()->State() != IClient::STATE_DEMOPLAYBACK && pPlayerInfo->m_Local) // Always render local input when not in demo playback
719 {
720 const auto &InputData = GameClient()->m_Controls.m_aInputData[g_Config.m_ClDummy];
721 Data.m_DirLeft = InputData.m_Direction == -1;
722 Data.m_DirJump = InputData.m_Jump == 1;
723 Data.m_DirRight = InputData.m_Direction == 1;
724 }
725 else
726 {
727 const auto &Character = GameClient()->m_Snap.m_aCharacters[pPlayerInfo->m_ClientId];
728 Data.m_DirLeft = Character.m_Cur.m_Direction == -1;
729 Data.m_DirJump = Character.m_Cur.m_Jumped & 1;
730 Data.m_DirRight = Character.m_Cur.m_Direction == 1;
731 }
732 }
733
734 Data.m_ShowHookStrongWeak = false;
735 Data.m_HookStrongWeakState = EHookStrongWeakState::NEUTRAL;
736 Data.m_ShowHookStrongWeakId = false;
737 Data.m_HookStrongWeakId = 0;
738
739 const bool Following = (GameClient()->m_Snap.m_SpecInfo.m_Active && !GameClient()->m_MultiViewActivated && GameClient()->m_Snap.m_SpecInfo.m_SpectatorId != SPEC_FREEVIEW);
740 if(GameClient()->m_Snap.m_LocalClientId != -1 || Following)
741 {
742 const int SelectedId = Following ? GameClient()->m_Snap.m_SpecInfo.m_SpectatorId : GameClient()->m_Snap.m_LocalClientId;
743 const CGameClient::CSnapState::CCharacterInfo &Selected = GameClient()->m_Snap.m_aCharacters[SelectedId];
744 const CGameClient::CSnapState::CCharacterInfo &Other = GameClient()->m_Snap.m_aCharacters[pPlayerInfo->m_ClientId];
745
746 if((Selected.m_HasExtendedData || GameClient()->m_aClients[SelectedId].m_SpecCharPresent) && Other.m_HasExtendedData)
747 {
748 int SelectedStrongWeakId = Selected.m_HasExtendedData ? Selected.m_ExtendedData.m_StrongWeakId : 0;
749 Data.m_HookStrongWeakId = Other.m_ExtendedData.m_StrongWeakId;
750 Data.m_ShowHookStrongWeakId = g_Config.m_Debug || g_Config.m_ClNamePlatesStrong == 2;
751 if(SelectedId == pPlayerInfo->m_ClientId)
752 {
753 Data.m_ShowHookStrongWeak = Data.m_ShowHookStrongWeakId;
754 }
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, std::max(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