1/* (c) Magnus Auvinen. See licence.txt in the root of the distribution for more information. */
2/* If you are missing that file, acquire a complete release at teeworlds.com. */
3#include "menus.h"
4#include "skins7.h"
5
6#include <base/math.h>
7#include <base/system.h>
8
9#include <engine/graphics.h>
10#include <engine/shared/config.h>
11#include <engine/shared/linereader.h>
12#include <engine/shared/localization.h>
13#include <engine/shared/protocol7.h>
14#include <engine/storage.h>
15#include <engine/textrender.h>
16#include <engine/updater.h>
17
18#include <generated/protocol.h>
19
20#include <game/client/animstate.h>
21#include <game/client/components/chat.h>
22#include <game/client/components/menu_background.h>
23#include <game/client/components/sounds.h>
24#include <game/client/gameclient.h>
25#include <game/client/skin.h>
26#include <game/client/ui.h>
27#include <game/client/ui_listbox.h>
28#include <game/client/ui_scrollregion.h>
29#include <game/localization.h>
30
31#include <vector>
32
33using namespace FontIcons;
34
35void CMenus::RenderSettingsTee7(CUIRect MainView)
36{
37 CUIRect SkinPreview, NormalSkinPreview, RedTeamSkinPreview, BlueTeamSkinPreview, Buttons, QuickSearch, DirectoryButton, RefreshButton, SaveDeleteButton, TabBars, TabBar, LeftTab, RightTab;
38 MainView.HSplitBottom(Cut: 20.0f, pTop: &MainView, pBottom: &Buttons);
39 MainView.HSplitBottom(Cut: 5.0f, pTop: &MainView, pBottom: nullptr);
40 Buttons.VSplitRight(Cut: 25.0f, pLeft: &Buttons, pRight: &RefreshButton);
41 Buttons.VSplitRight(Cut: 10.0f, pLeft: &Buttons, pRight: nullptr);
42 Buttons.VSplitRight(Cut: 140.0f, pLeft: &Buttons, pRight: &DirectoryButton);
43 Buttons.VSplitLeft(Cut: 220.0f, pLeft: &QuickSearch, pRight: &Buttons);
44 Buttons.VSplitLeft(Cut: 10.0f, pLeft: nullptr, pRight: &Buttons);
45 Buttons.VSplitLeft(Cut: 120.0f, pLeft: &SaveDeleteButton, pRight: &Buttons);
46 MainView.HSplitTop(Cut: 50.0f, pTop: &TabBars, pBottom: &MainView);
47 MainView.HSplitTop(Cut: 10.0f, pTop: nullptr, pBottom: &MainView);
48 TabBars.VSplitMid(pLeft: &TabBars, pRight: &SkinPreview, Spacing: 20.0f);
49
50 TabBars.HSplitTop(Cut: 20.0f, pTop: &TabBar, pBottom: &TabBars);
51 TabBar.VSplitMid(pLeft: &LeftTab, pRight: &RightTab);
52 TabBars.HSplitTop(Cut: 10.0f, pTop: nullptr, pBottom: &TabBars);
53
54 SkinPreview.Draw(Color: ColorRGBA(0.0f, 0.0f, 0.0f, 0.25f), Corners: IGraphics::CORNER_ALL, Rounding: 5.0f);
55 SkinPreview.VMargin(Cut: 10.0f, pOtherRect: &SkinPreview);
56 SkinPreview.VSplitRight(Cut: 50.0f, pLeft: &SkinPreview, pRight: &BlueTeamSkinPreview);
57 SkinPreview.VSplitRight(Cut: 10.0f, pLeft: &SkinPreview, pRight: nullptr);
58 SkinPreview.VSplitRight(Cut: 50.0f, pLeft: &SkinPreview, pRight: &RedTeamSkinPreview);
59 SkinPreview.VSplitRight(Cut: 10.0f, pLeft: &SkinPreview, pRight: nullptr);
60 SkinPreview.VSplitRight(Cut: 50.0f, pLeft: &SkinPreview, pRight: &NormalSkinPreview);
61 SkinPreview.VSplitRight(Cut: 10.0f, pLeft: &SkinPreview, pRight: nullptr);
62
63 static CButtonContainer s_PlayerTabButton;
64 if(DoButton_MenuTab(pButtonContainer: &s_PlayerTabButton, pText: Localize(pStr: "Player"), Checked: !m_Dummy, pRect: &LeftTab, Corners: IGraphics::CORNER_L, pAnimator: nullptr, pDefaultColor: nullptr, pActiveColor: nullptr, pHoverColor: nullptr, EdgeRounding: 4.0f))
65 {
66 m_Dummy = false;
67 }
68
69 static CButtonContainer s_DummyTabButton;
70 if(DoButton_MenuTab(pButtonContainer: &s_DummyTabButton, pText: Localize(pStr: "Dummy"), Checked: m_Dummy, pRect: &RightTab, Corners: IGraphics::CORNER_R, pAnimator: nullptr, pDefaultColor: nullptr, pActiveColor: nullptr, pHoverColor: nullptr, EdgeRounding: 4.0f))
71 {
72 m_Dummy = true;
73 }
74
75 TabBars.HSplitTop(Cut: 20.0f, pTop: &TabBar, pBottom: &TabBars);
76 TabBar.VSplitMid(pLeft: &LeftTab, pRight: &RightTab);
77
78 static CButtonContainer s_BasicTabButton;
79 if(DoButton_MenuTab(pButtonContainer: &s_BasicTabButton, pText: Localize(pStr: "Basic"), Checked: !m_CustomSkinMenu, pRect: &LeftTab, Corners: IGraphics::CORNER_L, pAnimator: nullptr, pDefaultColor: nullptr, pActiveColor: nullptr, pHoverColor: nullptr, EdgeRounding: 4.0f))
80 {
81 m_CustomSkinMenu = false;
82 }
83
84 static CButtonContainer s_CustomTabButton;
85 if(DoButton_MenuTab(pButtonContainer: &s_CustomTabButton, pText: Localize(pStr: "Custom"), Checked: m_CustomSkinMenu, pRect: &RightTab, Corners: IGraphics::CORNER_R, pAnimator: nullptr, pDefaultColor: nullptr, pActiveColor: nullptr, pHoverColor: nullptr, EdgeRounding: 4.0f))
86 {
87 m_CustomSkinMenu = true;
88 if(m_CustomSkinMenu && m_pSelectedSkin)
89 {
90 if(m_pSelectedSkin->m_Flags & CSkins7::SKINFLAG_STANDARD)
91 {
92 m_SkinNameInput.Set("copy_");
93 m_SkinNameInput.Append(pString: m_pSelectedSkin->m_aName);
94 }
95 else
96 m_SkinNameInput.Set(m_pSelectedSkin->m_aName);
97 }
98 }
99
100 // validate skin parts for solo mode
101 char aSkinParts[protocol7::NUM_SKINPARTS][protocol7::MAX_SKIN_ARRAY_SIZE];
102 char *apSkinPartsPtr[protocol7::NUM_SKINPARTS];
103 int aUCCVars[protocol7::NUM_SKINPARTS];
104 int aColorVars[protocol7::NUM_SKINPARTS];
105 for(int Part = 0; Part < protocol7::NUM_SKINPARTS; Part++)
106 {
107 str_copy(dst: aSkinParts[Part], src: CSkins7::ms_apSkinVariables[(int)m_Dummy][Part], dst_size: protocol7::MAX_SKIN_ARRAY_SIZE);
108 apSkinPartsPtr[Part] = aSkinParts[Part];
109 aUCCVars[Part] = *CSkins7::ms_apUCCVariables[(int)m_Dummy][Part];
110 aColorVars[Part] = *CSkins7::ms_apColorVariables[(int)m_Dummy][Part];
111 }
112 GameClient()->m_Skins7.ValidateSkinParts(apPartNames: apSkinPartsPtr, pUseCustomColors: aUCCVars, pPartColors: aColorVars, GameFlags: 0);
113
114 CTeeRenderInfo OwnSkinInfo;
115 OwnSkinInfo.m_Size = 50.0f;
116 for(int Part = 0; Part < protocol7::NUM_SKINPARTS; Part++)
117 {
118 GameClient()->m_Skins7.FindSkinPart(Part, pName: apSkinPartsPtr[Part], AllowSpecialPart: false)->ApplyTo(SixupRenderInfo&: OwnSkinInfo.m_aSixup[g_Config.m_ClDummy]);
119 GameClient()->m_Skins7.ApplyColorTo(SixupRenderInfo&: OwnSkinInfo.m_aSixup[g_Config.m_ClDummy], UseCustomColors: aUCCVars[Part], Value: aColorVars[Part], Part);
120 }
121
122 char aBuf[128 + IO_MAX_PATH_LENGTH];
123 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%s:", Localize(pStr: "Your skin"));
124 Ui()->DoLabel(pRect: &SkinPreview, pText: aBuf, Size: 14.0f, Align: TEXTALIGN_ML);
125
126 vec2 OffsetToMid;
127 CRenderTools::GetRenderTeeOffsetToRenderedTee(pAnim: CAnimState::GetIdle(), pInfo: &OwnSkinInfo, TeeOffsetToMid&: OffsetToMid);
128 {
129 // interactive tee: tee looking towards cursor, and it is happy when you touch it
130 const vec2 TeePosition = NormalSkinPreview.Center() + OffsetToMid;
131 const vec2 DeltaPosition = Ui()->MousePos() - TeePosition;
132 const float Distance = length(a: DeltaPosition);
133 const float InteractionDistance = 20.0f;
134 const vec2 TeeDirection = Distance < InteractionDistance ? normalize(v: vec2(DeltaPosition.x, maximum(a: DeltaPosition.y, b: 0.5f))) : normalize(v: DeltaPosition);
135 const int TeeEmote = Distance < InteractionDistance ? EMOTE_HAPPY : EMOTE_NORMAL;
136 RenderTools()->RenderTee(pAnim: CAnimState::GetIdle(), pInfo: &OwnSkinInfo, Emote: TeeEmote, Dir: TeeDirection, Pos: TeePosition);
137 static char s_InteractiveTeeButtonId;
138 if(Distance < InteractionDistance && Ui()->DoButtonLogic(pId: &s_InteractiveTeeButtonId, Checked: 0, pRect: &NormalSkinPreview, Flags: BUTTONFLAG_LEFT))
139 {
140 GameClient()->m_Sounds.Play(Channel: CSounds::CHN_GUI, SetId: SOUND_PLAYER_SPAWN, Volume: 1.0f);
141 }
142 }
143
144 // validate skin parts for team game mode
145 for(int Part = 0; Part < protocol7::NUM_SKINPARTS; Part++)
146 {
147 str_copy(dst: aSkinParts[Part], src: CSkins7::ms_apSkinVariables[(int)m_Dummy][Part], dst_size: protocol7::MAX_SKIN_ARRAY_SIZE);
148 apSkinPartsPtr[Part] = aSkinParts[Part];
149 aUCCVars[Part] = *CSkins7::ms_apUCCVariables[(int)m_Dummy][Part];
150 aColorVars[Part] = *CSkins7::ms_apColorVariables[(int)m_Dummy][Part];
151 }
152 GameClient()->m_Skins7.ValidateSkinParts(apPartNames: apSkinPartsPtr, pUseCustomColors: aUCCVars, pPartColors: aColorVars, GameFlags: GAMEFLAG_TEAMS);
153
154 CTeeRenderInfo TeamSkinInfo;
155 TeamSkinInfo.m_Size = OwnSkinInfo.m_Size;
156 for(int Part = 0; Part < protocol7::NUM_SKINPARTS; Part++)
157 {
158 GameClient()->m_Skins7.FindSkinPart(Part, pName: apSkinPartsPtr[Part], AllowSpecialPart: false)->ApplyTo(SixupRenderInfo&: TeamSkinInfo.m_aSixup[g_Config.m_ClDummy]);
159 TeamSkinInfo.m_aSixup[g_Config.m_ClDummy].m_aUseCustomColors[Part] = aUCCVars[Part];
160 }
161
162 for(int Part = 0; Part < protocol7::NUM_SKINPARTS; Part++)
163 {
164 TeamSkinInfo.m_aSixup[g_Config.m_ClDummy].m_aColors[Part] = GameClient()->m_Skins7.GetTeamColor(UseCustomColors: aUCCVars[Part], PartColor: aColorVars[Part], Team: TEAM_RED, Part);
165 }
166 RenderTools()->RenderTee(pAnim: CAnimState::GetIdle(), pInfo: &TeamSkinInfo, Emote: 0, Dir: vec2(1, 0), Pos: RedTeamSkinPreview.Center() + OffsetToMid);
167
168 for(int Part = 0; Part < protocol7::NUM_SKINPARTS; Part++)
169 {
170 TeamSkinInfo.m_aSixup[g_Config.m_ClDummy].m_aColors[Part] = GameClient()->m_Skins7.GetTeamColor(UseCustomColors: aUCCVars[Part], PartColor: aColorVars[Part], Team: TEAM_BLUE, Part);
171 }
172 RenderTools()->RenderTee(pAnim: CAnimState::GetIdle(), pInfo: &TeamSkinInfo, Emote: 0, Dir: vec2(-1, 0), Pos: BlueTeamSkinPreview.Center() + OffsetToMid);
173
174 if(m_CustomSkinMenu)
175 RenderSettingsTeeCustom7(MainView);
176 else
177 RenderSkinSelection7(MainView);
178
179 if(m_CustomSkinMenu)
180 {
181 static CButtonContainer s_CustomSkinSaveButton;
182 if(DoButton_Menu(pButtonContainer: &s_CustomSkinSaveButton, pText: Localize(pStr: "Save"), Checked: 0, pRect: &SaveDeleteButton))
183 {
184 m_Popup = POPUP_SAVE_SKIN;
185 m_SkinNameInput.SelectAll();
186 Ui()->SetActiveItem(&m_SkinNameInput);
187 }
188 }
189 else if(m_pSelectedSkin && (m_pSelectedSkin->m_Flags & CSkins7::SKINFLAG_STANDARD) == 0)
190 {
191 static CButtonContainer s_CustomSkinDeleteButton;
192 if(DoButton_Menu(pButtonContainer: &s_CustomSkinDeleteButton, pText: Localize(pStr: "Delete"), Checked: 0, pRect: &SaveDeleteButton) || Ui()->ConsumeHotkey(Hotkey: CUi::HOTKEY_DELETE))
193 {
194 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: Localize(pStr: "Are you sure that you want to delete '%s'?"), m_pSelectedSkin->m_aName);
195 PopupConfirm(pTitle: Localize(pStr: "Delete skin"), pMessage: aBuf, pConfirmButtonLabel: Localize(pStr: "Yes"), pCancelButtonLabel: Localize(pStr: "No"), pfnConfirmButtonCallback: &CMenus::PopupConfirmDeleteSkin7);
196 }
197 }
198
199 static CLineInput s_SkinFilterInput(g_Config.m_ClSkinFilterString, sizeof(g_Config.m_ClSkinFilterString));
200 if(Ui()->DoEditBox_Search(pLineInput: &s_SkinFilterInput, pRect: &QuickSearch, FontSize: 14.0f, HotkeyEnabled: !Ui()->IsPopupOpen() && !GameClient()->m_GameConsole.IsActive()))
201 {
202 m_SkinList7LastRefreshTime = std::nullopt;
203 m_SkinPartsList7LastRefreshTime = std::nullopt;
204 }
205
206 static CButtonContainer s_DirectoryButton;
207 if(DoButton_Menu(pButtonContainer: &s_DirectoryButton, pText: Localize(pStr: "Skins directory"), Checked: 0, pRect: &DirectoryButton))
208 {
209 Storage()->GetCompletePath(Type: IStorage::TYPE_SAVE, pDir: "skins7", pBuffer: aBuf, BufferSize: sizeof(aBuf));
210 Storage()->CreateFolder(pFoldername: "skins7", Type: IStorage::TYPE_SAVE);
211 Client()->ViewFile(pFilename: aBuf);
212 }
213 GameClient()->m_Tooltips.DoToolTip(pId: &s_DirectoryButton, pNearRect: &DirectoryButton, pText: Localize(pStr: "Open the directory to add custom skins"));
214
215 TextRender()->SetFontPreset(EFontPreset::ICON_FONT);
216 TextRender()->SetRenderFlags(ETextRenderFlags::TEXT_RENDER_FLAG_ONLY_ADVANCE_WIDTH | ETextRenderFlags::TEXT_RENDER_FLAG_NO_X_BEARING | ETextRenderFlags::TEXT_RENDER_FLAG_NO_Y_BEARING | ETextRenderFlags::TEXT_RENDER_FLAG_NO_PIXEL_ALIGNMENT | ETextRenderFlags::TEXT_RENDER_FLAG_NO_OVERSIZE);
217 static CButtonContainer s_SkinRefreshButton;
218 if(DoButton_Menu(pButtonContainer: &s_SkinRefreshButton, pText: FONT_ICON_ARROW_ROTATE_RIGHT, Checked: 0, pRect: &RefreshButton) ||
219 (!Ui()->IsPopupOpen() && !GameClient()->m_GameConsole.IsActive() && (Input()->KeyPress(Key: KEY_F5) || (Input()->ModifierIsPressed() && Input()->KeyPress(Key: KEY_R)))))
220 {
221 // reset render flags for possible loading screen
222 TextRender()->SetRenderFlags(0);
223 TextRender()->SetFontPreset(EFontPreset::DEFAULT_FONT);
224 GameClient()->RefreshSkins(SkinDescriptorFlags: CSkinDescriptor::FLAG_SEVEN);
225 }
226 TextRender()->SetRenderFlags(0);
227 TextRender()->SetFontPreset(EFontPreset::DEFAULT_FONT);
228}
229
230void CMenus::PopupConfirmDeleteSkin7()
231{
232 dbg_assert(m_pSelectedSkin, "no skin selected for deletion");
233
234 if(!GameClient()->m_Skins7.RemoveSkin(pSkin: m_pSelectedSkin))
235 {
236 PopupMessage(pTitle: Localize(pStr: "Error"), pMessage: Localize(pStr: "Unable to delete skin"), pButtonLabel: Localize(pStr: "Ok"));
237 return;
238 }
239 m_pSelectedSkin = nullptr;
240}
241
242void CMenus::RenderSettingsTeeCustom7(CUIRect MainView)
243{
244 CUIRect ButtonBar, SkinPartSelection, CustomColors;
245
246 MainView.HSplitTop(Cut: 20.0f, pTop: &ButtonBar, pBottom: &MainView);
247 MainView.Draw(Color: ColorRGBA(0.0f, 0.0f, 0.0f, 0.25f), Corners: IGraphics::CORNER_B, Rounding: 5.0f);
248 MainView.VSplitMid(pLeft: &SkinPartSelection, pRight: &CustomColors, Spacing: 10.0f);
249 CustomColors.Margin(Cut: 5.0f, pOtherRect: &CustomColors);
250 CUIRect CustomColorsButton, RandomSkinButton;
251 CustomColors.HSplitTop(Cut: 20.0f, pTop: &CustomColorsButton, pBottom: &CustomColors);
252 CustomColorsButton.VSplitRight(Cut: 30.0f, pLeft: &CustomColorsButton, pRight: &RandomSkinButton);
253 CustomColorsButton.VSplitRight(Cut: 20.0f, pLeft: &CustomColorsButton, pRight: nullptr);
254
255 const float ButtonWidth = ButtonBar.w / protocol7::NUM_SKINPARTS;
256
257 static CButtonContainer s_aSkinPartButtons[protocol7::NUM_SKINPARTS];
258 for(int i = 0; i < protocol7::NUM_SKINPARTS; i++)
259 {
260 CUIRect Button;
261 ButtonBar.VSplitLeft(Cut: ButtonWidth, pLeft: &Button, pRight: &ButtonBar);
262 const int Corners = i == 0 ? IGraphics::CORNER_TL : (i == (protocol7::NUM_SKINPARTS - 1) ? IGraphics::CORNER_TR : IGraphics::CORNER_NONE);
263 if(DoButton_MenuTab(pButtonContainer: &s_aSkinPartButtons[i], pText: Localize(pStr: CSkins7::ms_apSkinPartNamesLocalized[i], pContext: "skins"), Checked: m_TeePartSelected == i, pRect: &Button, Corners, pAnimator: nullptr, pDefaultColor: nullptr, pActiveColor: nullptr, pHoverColor: nullptr, EdgeRounding: 4.0f))
264 {
265 m_TeePartSelected = i;
266 }
267 }
268
269 RenderSkinPartSelection7(MainView: SkinPartSelection);
270
271 int *pUseCustomColor = CSkins7::ms_apUCCVariables[(int)m_Dummy][m_TeePartSelected];
272 if(DoButton_CheckBox(pId: pUseCustomColor, pText: Localize(pStr: "Custom colors"), Checked: *pUseCustomColor, pRect: &CustomColorsButton))
273 {
274 *pUseCustomColor = !*pUseCustomColor;
275 SetNeedSendInfo();
276 }
277
278 if(*pUseCustomColor)
279 {
280 CUIRect CustomColorScrollbars;
281 CustomColors.HSplitTop(Cut: 5.0f, pTop: nullptr, pBottom: &CustomColors);
282 CustomColors.HSplitTop(Cut: 95.0f, pTop: &CustomColorScrollbars, pBottom: &CustomColors);
283
284 if(RenderHslaScrollbars(pRect: &CustomColorScrollbars, pColor: CSkins7::ms_apColorVariables[(int)m_Dummy][m_TeePartSelected], Alpha: m_TeePartSelected == protocol7::SKINPART_MARKING, DarkestLight: ColorHSLA::DARKEST_LGT7))
285 {
286 SetNeedSendInfo();
287 }
288 }
289
290 // Random skin button
291 static CButtonContainer s_RandomSkinButton;
292 static const char *s_apDice[] = {FONT_ICON_DICE_ONE, FONT_ICON_DICE_TWO, FONT_ICON_DICE_THREE, FONT_ICON_DICE_FOUR, FONT_ICON_DICE_FIVE, FONT_ICON_DICE_SIX};
293 static int s_CurrentDie = rand() % std::size(s_apDice);
294 TextRender()->SetFontPreset(EFontPreset::ICON_FONT);
295 TextRender()->SetRenderFlags(ETextRenderFlags::TEXT_RENDER_FLAG_ONLY_ADVANCE_WIDTH | ETextRenderFlags::TEXT_RENDER_FLAG_NO_X_BEARING | ETextRenderFlags::TEXT_RENDER_FLAG_NO_Y_BEARING | ETextRenderFlags::TEXT_RENDER_FLAG_NO_PIXEL_ALIGNMENT | ETextRenderFlags::TEXT_RENDER_FLAG_NO_OVERSIZE);
296 if(DoButton_Menu(pButtonContainer: &s_RandomSkinButton, pText: s_apDice[s_CurrentDie], Checked: 0, pRect: &RandomSkinButton, Flags: BUTTONFLAG_LEFT, pImageName: nullptr, Corners: IGraphics::CORNER_ALL, Rounding: 5.0f, FontFactor: -0.2f))
297 {
298 GameClient()->m_Skins7.RandomizeSkin(Dummy: m_Dummy);
299 SetNeedSendInfo();
300 s_CurrentDie = rand() % std::size(s_apDice);
301 }
302 TextRender()->SetRenderFlags(0);
303 TextRender()->SetFontPreset(EFontPreset::DEFAULT_FONT);
304 GameClient()->m_Tooltips.DoToolTip(pId: &s_RandomSkinButton, pNearRect: &RandomSkinButton, pText: Localize(pStr: "Create a random skin"));
305}
306
307void CMenus::RenderSkinSelection7(CUIRect MainView)
308{
309 static float s_LastSelectionTime = -10.0f;
310 static std::vector<const CSkins7::CSkin *> s_vpSkinList;
311 static CListBox s_ListBox;
312
313 if(!m_SkinList7LastRefreshTime.has_value() || m_SkinList7LastRefreshTime.value() != m_SkinList7LastRefreshTime)
314 {
315 s_vpSkinList.clear();
316 for(const CSkins7::CSkin &Skin : GameClient()->m_Skins7.GetSkins())
317 {
318 if((Skin.m_Flags & CSkins7::SKINFLAG_SPECIAL) != 0)
319 continue;
320 if(g_Config.m_ClSkinFilterString[0] != '\0' && !str_utf8_find_nocase(haystack: Skin.m_aName, needle: g_Config.m_ClSkinFilterString))
321 continue;
322
323 s_vpSkinList.emplace_back(args: &Skin);
324 }
325 }
326
327 m_pSelectedSkin = nullptr;
328 int OldSelected = -1;
329 for(int i = 0; i < (int)s_vpSkinList.size(); ++i)
330 {
331 const CSkins7::CSkin *pSkin = s_vpSkinList[i];
332 if(!str_comp(a: pSkin->m_aName, b: CSkins7::ms_apSkinNameVariables[m_Dummy]))
333 {
334 m_pSelectedSkin = pSkin;
335 OldSelected = i;
336 break;
337 }
338 }
339 s_ListBox.DoStart(RowHeight: 50.0f, NumItems: s_vpSkinList.size(), ItemsPerRow: 4, RowsPerScroll: 1, SelectedIndex: OldSelected, pRect: &MainView);
340
341 for(const CSkins7::CSkin *pSkin : s_vpSkinList)
342 {
343 const CListboxItem Item = s_ListBox.DoNextItem(pId: pSkin);
344 if(!Item.m_Visible)
345 continue;
346
347 CUIRect TeePreview, Label;
348 Item.m_Rect.VSplitLeft(Cut: 60.0f, pLeft: &TeePreview, pRight: &Label);
349
350 CTeeRenderInfo Info;
351 for(int Part = 0; Part < protocol7::NUM_SKINPARTS; Part++)
352 {
353 pSkin->m_apParts[Part]->ApplyTo(SixupRenderInfo&: Info.m_aSixup[g_Config.m_ClDummy]);
354 GameClient()->m_Skins7.ApplyColorTo(SixupRenderInfo&: Info.m_aSixup[g_Config.m_ClDummy], UseCustomColors: pSkin->m_aUseCustomColors[Part], Value: pSkin->m_aPartColors[Part], Part);
355 }
356 Info.m_Size = 50.0f;
357
358 {
359 // interactive tee: tee is happy to be selected
360 int TeeEmote = (Item.m_Selected && s_LastSelectionTime + 0.75f > Client()->GlobalTime()) ? EMOTE_HAPPY : EMOTE_NORMAL;
361 vec2 OffsetToMid;
362 CRenderTools::GetRenderTeeOffsetToRenderedTee(pAnim: CAnimState::GetIdle(), pInfo: &Info, TeeOffsetToMid&: OffsetToMid);
363 RenderTools()->RenderTee(pAnim: CAnimState::GetIdle(), pInfo: &Info, Emote: TeeEmote, Dir: vec2(1.0f, 0.0f), Pos: TeePreview.Center() + OffsetToMid);
364 }
365
366 SLabelProperties Props;
367 Props.m_MaxWidth = Label.w - 5.0f;
368 Ui()->DoLabel(pRect: &Label, pText: pSkin->m_aName, Size: 12.0f, Align: TEXTALIGN_ML, LabelProps: Props);
369 }
370
371 const int NewSelected = s_ListBox.DoEnd();
372 if(NewSelected != -1 && NewSelected != OldSelected)
373 {
374 s_LastSelectionTime = Client()->GlobalTime();
375 m_pSelectedSkin = s_vpSkinList[NewSelected];
376 str_copy(dst: CSkins7::ms_apSkinNameVariables[m_Dummy], src: m_pSelectedSkin->m_aName, dst_size: protocol7::MAX_SKIN_ARRAY_SIZE);
377 for(int Part = 0; Part < protocol7::NUM_SKINPARTS; Part++)
378 {
379 str_copy(dst: CSkins7::ms_apSkinVariables[(int)m_Dummy][Part], src: m_pSelectedSkin->m_apParts[Part]->m_aName, dst_size: protocol7::MAX_SKIN_ARRAY_SIZE);
380 *CSkins7::ms_apUCCVariables[(int)m_Dummy][Part] = m_pSelectedSkin->m_aUseCustomColors[Part];
381 *CSkins7::ms_apColorVariables[(int)m_Dummy][Part] = m_pSelectedSkin->m_aPartColors[Part];
382 }
383 SetNeedSendInfo();
384 }
385}
386
387void CMenus::RenderSkinPartSelection7(CUIRect MainView)
388{
389 static std::vector<const CSkins7::CSkinPart *> s_avpList[protocol7::NUM_SKINPARTS];
390 static CListBox s_ListBox;
391 for(int Part = 0; Part < protocol7::NUM_SKINPARTS; Part++)
392 {
393 if(!m_SkinList7LastRefreshTime.has_value() || m_SkinList7LastRefreshTime.value() != GameClient()->m_Skins7.LastRefreshTime())
394 {
395 s_avpList[Part].clear();
396 for(const CSkins7::CSkinPart &SkinPart : GameClient()->m_Skins7.GetSkinParts(Part))
397 {
398 if((SkinPart.m_Flags & CSkins7::SKINFLAG_SPECIAL) != 0)
399 continue;
400
401 if(g_Config.m_ClSkinFilterString[0] != '\0' && !str_utf8_find_nocase(haystack: SkinPart.m_aName, needle: g_Config.m_ClSkinFilterString))
402 continue;
403
404 s_avpList[Part].emplace_back(args: &SkinPart);
405 }
406 }
407 }
408
409 int OldSelected = -1;
410 for(int i = 0; i < (int)s_avpList[m_TeePartSelected].size(); ++i)
411 {
412 const CSkins7::CSkinPart *pPart = s_avpList[m_TeePartSelected][i];
413 if(!str_comp(a: pPart->m_aName, b: CSkins7::ms_apSkinVariables[(int)m_Dummy][m_TeePartSelected]))
414 {
415 OldSelected = i;
416 break;
417 }
418 }
419 s_ListBox.DoStart(RowHeight: 72.0f, NumItems: s_avpList[m_TeePartSelected].size(), ItemsPerRow: 4, RowsPerScroll: 1, SelectedIndex: OldSelected, pRect: &MainView, Background: false, BackgroundCorners: IGraphics::CORNER_NONE, ForceShowScrollbar: true);
420
421 for(const CSkins7::CSkinPart *pPart : s_avpList[m_TeePartSelected])
422 {
423 CListboxItem Item = s_ListBox.DoNextItem(pId: pPart);
424 if(!Item.m_Visible)
425 continue;
426
427 CUIRect Label;
428 Item.m_Rect.Margin(Cut: 5.0f, pOtherRect: &Item.m_Rect);
429 Item.m_Rect.HSplitBottom(Cut: 12.0f, pTop: &Item.m_Rect, pBottom: &Label);
430
431 CTeeRenderInfo Info;
432 for(int Part = 0; Part < protocol7::NUM_SKINPARTS; Part++)
433 {
434 const CSkins7::CSkinPart *pPreviewPart = (m_TeePartSelected == Part ? pPart : GameClient()->m_Skins7.FindSkinPart(Part, pName: CSkins7::ms_apSkinVariables[(int)m_Dummy][Part], AllowSpecialPart: false));
435 pPreviewPart->ApplyTo(SixupRenderInfo&: Info.m_aSixup[g_Config.m_ClDummy]);
436 GameClient()->m_Skins7.ApplyColorTo(SixupRenderInfo&: Info.m_aSixup[g_Config.m_ClDummy], UseCustomColors: *CSkins7::ms_apUCCVariables[(int)m_Dummy][Part], Value: *CSkins7::ms_apColorVariables[(int)m_Dummy][Part], Part);
437 }
438 Info.m_Size = 50.0f;
439
440 vec2 OffsetToMid;
441 CRenderTools::GetRenderTeeOffsetToRenderedTee(pAnim: CAnimState::GetIdle(), pInfo: &Info, TeeOffsetToMid&: OffsetToMid);
442 const vec2 TeePos = Item.m_Rect.Center() + OffsetToMid;
443 if(m_TeePartSelected == protocol7::SKINPART_HANDS)
444 {
445 // RenderTools()->RenderTeeHand(&Info, TeePos, vec2(1.0f, 0.0f), -pi*0.5f, vec2(18, 0));
446 }
447 int TeePartEmote = EMOTE_NORMAL;
448 if(m_TeePartSelected == protocol7::SKINPART_EYES)
449 {
450 TeePartEmote = (int)(Client()->GlobalTime() * 0.5f) % NUM_EMOTES;
451 }
452 RenderTools()->RenderTee(pAnim: CAnimState::GetIdle(), pInfo: &Info, Emote: TeePartEmote, Dir: vec2(1.0f, 0.0f), Pos: TeePos);
453
454 Ui()->DoLabel(pRect: &Label, pText: pPart->m_aName, Size: 12.0f, Align: TEXTALIGN_MC);
455 }
456
457 const int NewSelected = s_ListBox.DoEnd();
458 if(NewSelected != -1 && NewSelected != OldSelected)
459 {
460 str_copy(dst: CSkins7::ms_apSkinVariables[(int)m_Dummy][m_TeePartSelected], src: s_avpList[m_TeePartSelected][NewSelected]->m_aName, dst_size: protocol7::MAX_SKIN_ARRAY_SIZE);
461 CSkins7::ms_apSkinNameVariables[m_Dummy][0] = '\0';
462 SetNeedSendInfo();
463 }
464}
465