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