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