1#include "menus.h"
2
3#include <base/dbg.h>
4#include <base/str.h>
5#include <base/time.h>
6
7#include <engine/font_icons.h>
8#include <engine/shared/config.h>
9#include <engine/storage.h>
10#include <engine/textrender.h>
11
12#include <game/client/gameclient.h>
13#include <game/client/ui_listbox.h>
14#include <game/localization.h>
15
16#include <chrono>
17
18using namespace std::chrono_literals;
19
20typedef std::function<void()> TMenuAssetScanLoadedFunc;
21
22struct SMenuAssetScanUser
23{
24 void *m_pUser;
25 TMenuAssetScanLoadedFunc m_LoadedFunc;
26};
27
28// IDs of the tabs in the Assets menu
29enum
30{
31 ASSETS_TAB_ENTITIES = 0,
32 ASSETS_TAB_GAME = 1,
33 ASSETS_TAB_EMOTICONS = 2,
34 ASSETS_TAB_PARTICLES = 3,
35 ASSETS_TAB_HUD = 4,
36 ASSETS_TAB_EXTRAS = 5,
37 NUMBER_OF_ASSETS_TABS = 6,
38};
39
40void CMenus::LoadEntities(SCustomEntities *pEntitiesItem, void *pUser)
41{
42 auto *pRealUser = (SMenuAssetScanUser *)pUser;
43 auto *pThis = (CMenus *)pRealUser->m_pUser;
44
45 char aPath[IO_MAX_PATH_LENGTH];
46 if(str_comp(a: pEntitiesItem->m_aName, b: "default") == 0)
47 {
48 for(int i = 0; i < MAP_IMAGE_MOD_TYPE_COUNT; ++i)
49 {
50 str_format(buffer: aPath, buffer_size: sizeof(aPath), format: "editor/entities_clear/%s.png", gs_apModEntitiesNames[i]);
51 pEntitiesItem->m_aImages[i].m_Texture = pThis->Graphics()->LoadTexture(pFilename: aPath, StorageType: IStorage::TYPE_ALL);
52 if(!pEntitiesItem->m_RenderTexture.IsValid() || pEntitiesItem->m_RenderTexture.IsNullTexture())
53 pEntitiesItem->m_RenderTexture = pEntitiesItem->m_aImages[i].m_Texture;
54 }
55 }
56 else
57 {
58 for(int i = 0; i < MAP_IMAGE_MOD_TYPE_COUNT; ++i)
59 {
60 str_format(buffer: aPath, buffer_size: sizeof(aPath), format: "assets/entities/%s/%s.png", pEntitiesItem->m_aName, gs_apModEntitiesNames[i]);
61 pEntitiesItem->m_aImages[i].m_Texture = pThis->Graphics()->LoadTexture(pFilename: aPath, StorageType: IStorage::TYPE_ALL);
62 if(pEntitiesItem->m_aImages[i].m_Texture.IsNullTexture())
63 {
64 str_format(buffer: aPath, buffer_size: sizeof(aPath), format: "assets/entities/%s.png", pEntitiesItem->m_aName);
65 pEntitiesItem->m_aImages[i].m_Texture = pThis->Graphics()->LoadTexture(pFilename: aPath, StorageType: IStorage::TYPE_ALL);
66 }
67 if(!pEntitiesItem->m_RenderTexture.IsValid() || pEntitiesItem->m_RenderTexture.IsNullTexture())
68 pEntitiesItem->m_RenderTexture = pEntitiesItem->m_aImages[i].m_Texture;
69 }
70 }
71}
72
73int CMenus::EntitiesScan(const char *pName, int IsDir, int DirType, void *pUser)
74{
75 auto *pRealUser = (SMenuAssetScanUser *)pUser;
76 auto *pThis = (CMenus *)pRealUser->m_pUser;
77 if(IsDir)
78 {
79 if(pName[0] == '.')
80 return 0;
81
82 // default is reserved
83 if(str_comp(a: pName, b: "default") == 0)
84 return 0;
85
86 SCustomEntities EntitiesItem;
87 str_copy(dst&: EntitiesItem.m_aName, src: pName);
88 CMenus::LoadEntities(pEntitiesItem: &EntitiesItem, pUser);
89 pThis->m_vEntitiesList.push_back(x: EntitiesItem);
90 }
91 else
92 {
93 if(str_endswith(str: pName, suffix: ".png"))
94 {
95 char aName[IO_MAX_PATH_LENGTH];
96 str_truncate(dst: aName, dst_size: sizeof(aName), src: pName, truncation_len: str_length(str: pName) - 4);
97 // default is reserved
98 if(str_comp(a: aName, b: "default") == 0)
99 return 0;
100
101 SCustomEntities EntitiesItem;
102 str_copy(dst&: EntitiesItem.m_aName, src: aName);
103 CMenus::LoadEntities(pEntitiesItem: &EntitiesItem, pUser);
104 pThis->m_vEntitiesList.push_back(x: EntitiesItem);
105 }
106 }
107
108 pRealUser->m_LoadedFunc();
109
110 return 0;
111}
112
113template<typename TName>
114static void LoadAsset(TName *pAssetItem, const char *pAssetName, IGraphics *pGraphics)
115{
116 char aPath[IO_MAX_PATH_LENGTH];
117 if(str_comp(pAssetItem->m_aName, "default") == 0)
118 {
119 str_format(buffer: aPath, buffer_size: sizeof(aPath), format: "%s.png", pAssetName);
120 pAssetItem->m_RenderTexture = pGraphics->LoadTexture(pFilename: aPath, StorageType: IStorage::TYPE_ALL);
121 }
122 else
123 {
124 str_format(aPath, sizeof(aPath), "assets/%s/%s.png", pAssetName, pAssetItem->m_aName);
125 pAssetItem->m_RenderTexture = pGraphics->LoadTexture(pFilename: aPath, StorageType: IStorage::TYPE_ALL);
126 if(pAssetItem->m_RenderTexture.IsNullTexture())
127 {
128 str_format(aPath, sizeof(aPath), "assets/%s/%s/%s.png", pAssetName, pAssetItem->m_aName, pAssetName);
129 pAssetItem->m_RenderTexture = pGraphics->LoadTexture(pFilename: aPath, StorageType: IStorage::TYPE_ALL);
130 }
131 }
132}
133
134template<typename TName>
135static int AssetScan(const char *pName, int IsDir, int DirType, std::vector<TName> &vAssetList, const char *pAssetName, IGraphics *pGraphics, void *pUser)
136{
137 auto *pRealUser = (SMenuAssetScanUser *)pUser;
138 if(IsDir)
139 {
140 if(pName[0] == '.')
141 return 0;
142
143 // default is reserved
144 if(str_comp(a: pName, b: "default") == 0)
145 return 0;
146
147 TName AssetItem;
148 str_copy(AssetItem.m_aName, pName);
149 LoadAsset(&AssetItem, pAssetName, pGraphics);
150 vAssetList.push_back(AssetItem);
151 }
152 else
153 {
154 if(str_endswith(str: pName, suffix: ".png"))
155 {
156 char aName[IO_MAX_PATH_LENGTH];
157 str_truncate(dst: aName, dst_size: sizeof(aName), src: pName, truncation_len: str_length(str: pName) - 4);
158 // default is reserved
159 if(str_comp(a: aName, b: "default") == 0)
160 return 0;
161
162 TName AssetItem;
163 str_copy(AssetItem.m_aName, aName);
164 LoadAsset(&AssetItem, pAssetName, pGraphics);
165 vAssetList.push_back(AssetItem);
166 }
167 }
168
169 pRealUser->m_LoadedFunc();
170
171 return 0;
172}
173
174int CMenus::GameScan(const char *pName, int IsDir, int DirType, void *pUser)
175{
176 auto *pRealUser = (SMenuAssetScanUser *)pUser;
177 auto *pThis = (CMenus *)pRealUser->m_pUser;
178 IGraphics *pGraphics = pThis->Graphics();
179 return AssetScan(pName, IsDir, DirType, vAssetList&: pThis->m_vGameList, pAssetName: "game", pGraphics, pUser);
180}
181
182int CMenus::EmoticonsScan(const char *pName, int IsDir, int DirType, void *pUser)
183{
184 auto *pRealUser = (SMenuAssetScanUser *)pUser;
185 auto *pThis = (CMenus *)pRealUser->m_pUser;
186 IGraphics *pGraphics = pThis->Graphics();
187 return AssetScan(pName, IsDir, DirType, vAssetList&: pThis->m_vEmoticonList, pAssetName: "emoticons", pGraphics, pUser);
188}
189
190int CMenus::ParticlesScan(const char *pName, int IsDir, int DirType, void *pUser)
191{
192 auto *pRealUser = (SMenuAssetScanUser *)pUser;
193 auto *pThis = (CMenus *)pRealUser->m_pUser;
194 IGraphics *pGraphics = pThis->Graphics();
195 return AssetScan(pName, IsDir, DirType, vAssetList&: pThis->m_vParticlesList, pAssetName: "particles", pGraphics, pUser);
196}
197
198int CMenus::HudScan(const char *pName, int IsDir, int DirType, void *pUser)
199{
200 auto *pRealUser = (SMenuAssetScanUser *)pUser;
201 auto *pThis = (CMenus *)pRealUser->m_pUser;
202 IGraphics *pGraphics = pThis->Graphics();
203 return AssetScan(pName, IsDir, DirType, vAssetList&: pThis->m_vHudList, pAssetName: "hud", pGraphics, pUser);
204}
205
206int CMenus::ExtrasScan(const char *pName, int IsDir, int DirType, void *pUser)
207{
208 auto *pRealUser = (SMenuAssetScanUser *)pUser;
209 auto *pThis = (CMenus *)pRealUser->m_pUser;
210 IGraphics *pGraphics = pThis->Graphics();
211 return AssetScan(pName, IsDir, DirType, vAssetList&: pThis->m_vExtrasList, pAssetName: "extras", pGraphics, pUser);
212}
213
214static std::vector<const CMenus::SCustomEntities *> gs_vpSearchEntitiesList;
215static std::vector<const CMenus::SCustomGame *> gs_vpSearchGamesList;
216static std::vector<const CMenus::SCustomEmoticon *> gs_vpSearchEmoticonsList;
217static std::vector<const CMenus::SCustomParticle *> gs_vpSearchParticlesList;
218static std::vector<const CMenus::SCustomHud *> gs_vpSearchHudList;
219static std::vector<const CMenus::SCustomExtras *> gs_vpSearchExtrasList;
220
221static bool gs_aInitCustomList[NUMBER_OF_ASSETS_TABS] = {
222 true,
223};
224
225static size_t gs_aCustomListSize[NUMBER_OF_ASSETS_TABS] = {
226 0,
227};
228
229static CLineInputBuffered<64> s_aFilterInputs[NUMBER_OF_ASSETS_TABS];
230
231static int s_CurCustomTab = ASSETS_TAB_ENTITIES;
232
233static const CMenus::SCustomItem *GetCustomItem(int CurTab, size_t Index)
234{
235 if(CurTab == ASSETS_TAB_ENTITIES)
236 return gs_vpSearchEntitiesList[Index];
237 else if(CurTab == ASSETS_TAB_GAME)
238 return gs_vpSearchGamesList[Index];
239 else if(CurTab == ASSETS_TAB_EMOTICONS)
240 return gs_vpSearchEmoticonsList[Index];
241 else if(CurTab == ASSETS_TAB_PARTICLES)
242 return gs_vpSearchParticlesList[Index];
243 else if(CurTab == ASSETS_TAB_HUD)
244 return gs_vpSearchHudList[Index];
245 else if(CurTab == ASSETS_TAB_EXTRAS)
246 return gs_vpSearchExtrasList[Index];
247 dbg_assert_failed("Invalid CurTab: %d", CurTab);
248}
249
250template<typename TName>
251static void ClearAssetList(std::vector<TName> &vList, IGraphics *pGraphics)
252{
253 for(TName &Asset : vList)
254 {
255 pGraphics->UnloadTexture(pIndex: &Asset.m_RenderTexture);
256 }
257 vList.clear();
258}
259
260void CMenus::ClearCustomItems(int CurTab)
261{
262 if(CurTab == ASSETS_TAB_ENTITIES)
263 {
264 for(auto &Entity : m_vEntitiesList)
265 {
266 for(auto &Image : Entity.m_aImages)
267 {
268 Graphics()->UnloadTexture(pIndex: &Image.m_Texture);
269 }
270 }
271 m_vEntitiesList.clear();
272
273 // reload current entities
274 GameClient()->m_MapImages.ChangeEntitiesPath(pPath: g_Config.m_ClAssetsEntities);
275 }
276 else if(CurTab == ASSETS_TAB_GAME)
277 {
278 ClearAssetList(vList&: m_vGameList, pGraphics: Graphics());
279
280 // reload current game skin
281 GameClient()->LoadGameSkin(pPath: g_Config.m_ClAssetGame);
282 }
283 else if(CurTab == ASSETS_TAB_EMOTICONS)
284 {
285 ClearAssetList(vList&: m_vEmoticonList, pGraphics: Graphics());
286
287 // reload current emoticons skin
288 GameClient()->LoadEmoticonsSkin(pPath: g_Config.m_ClAssetEmoticons);
289 }
290 else if(CurTab == ASSETS_TAB_PARTICLES)
291 {
292 ClearAssetList(vList&: m_vParticlesList, pGraphics: Graphics());
293
294 // reload current particles skin
295 GameClient()->LoadParticlesSkin(pPath: g_Config.m_ClAssetParticles);
296 }
297 else if(CurTab == ASSETS_TAB_HUD)
298 {
299 ClearAssetList(vList&: m_vHudList, pGraphics: Graphics());
300
301 // reload current hud skin
302 GameClient()->LoadHudSkin(pPath: g_Config.m_ClAssetHud);
303 }
304 else if(CurTab == ASSETS_TAB_EXTRAS)
305 {
306 ClearAssetList(vList&: m_vExtrasList, pGraphics: Graphics());
307
308 // reload current DDNet particles skin
309 GameClient()->LoadExtrasSkin(pPath: g_Config.m_ClAssetExtras);
310 }
311 else
312 {
313 dbg_assert_failed("Invalid CurTab: %d", CurTab);
314 }
315 gs_aInitCustomList[CurTab] = true;
316}
317
318template<typename TName, typename TCaller>
319static void InitAssetList(std::vector<TName> &vAssetList, const char *pAssetPath, const char *pAssetName, FS_LISTDIR_CALLBACK pfnCallback, IGraphics *pGraphics, IStorage *pStorage, TCaller Caller)
320{
321 if(vAssetList.empty())
322 {
323 TName AssetItem;
324 str_copy(AssetItem.m_aName, "default");
325 LoadAsset(&AssetItem, pAssetName, pGraphics);
326 vAssetList.push_back(AssetItem);
327
328 // load assets
329 pStorage->ListDirectory(Type: IStorage::TYPE_ALL, pPath: pAssetPath, pfnCallback, pUser: Caller);
330 std::sort(vAssetList.begin(), vAssetList.end());
331 }
332 if(vAssetList.size() != gs_aCustomListSize[s_CurCustomTab])
333 gs_aInitCustomList[s_CurCustomTab] = true;
334}
335
336template<typename TName>
337static int InitSearchList(std::vector<const TName *> &vpSearchList, std::vector<TName> &vAssetList)
338{
339 vpSearchList.clear();
340 int ListSize = vAssetList.size();
341 for(int i = 0; i < ListSize; ++i)
342 {
343 const TName *pAsset = &vAssetList[i];
344
345 // filter quick search
346 if(!s_aFilterInputs[s_CurCustomTab].IsEmpty() && !str_utf8_find_nocase(pAsset->m_aName, s_aFilterInputs[s_CurCustomTab].GetString()))
347 continue;
348
349 vpSearchList.push_back(pAsset);
350 }
351 return vAssetList.size();
352}
353
354void CMenus::RenderSettingsCustom(CUIRect MainView)
355{
356 CUIRect TabBar, CustomList, QuickSearch, DirectoryButton, ReloadButton;
357
358 MainView.HSplitTop(Cut: 20.0f, pTop: &TabBar, pBottom: &MainView);
359 const float TabWidth = TabBar.w / (float)NUMBER_OF_ASSETS_TABS;
360 static CButtonContainer s_aPageTabs[NUMBER_OF_ASSETS_TABS] = {};
361 const char *apTabNames[NUMBER_OF_ASSETS_TABS] = {
362 Localize(pStr: "Entities"),
363 Localize(pStr: "Game"),
364 Localize(pStr: "Emoticons"),
365 Localize(pStr: "Particles"),
366 Localize(pStr: "HUD"),
367 Localize(pStr: "Extras")};
368
369 for(int Tab = ASSETS_TAB_ENTITIES; Tab < NUMBER_OF_ASSETS_TABS; ++Tab)
370 {
371 CUIRect Button;
372 TabBar.VSplitLeft(Cut: TabWidth, pLeft: &Button, pRight: &TabBar);
373 const int Corners = Tab == ASSETS_TAB_ENTITIES ? IGraphics::CORNER_L : (Tab == NUMBER_OF_ASSETS_TABS - 1 ? IGraphics::CORNER_R : IGraphics::CORNER_NONE);
374 if(DoButton_MenuTab(pButtonContainer: &s_aPageTabs[Tab], pText: apTabNames[Tab], Checked: s_CurCustomTab == Tab, pRect: &Button, Corners, pAnimator: nullptr, pDefaultColor: nullptr, pActiveColor: nullptr, pHoverColor: nullptr, EdgeRounding: 4.0f))
375 {
376 s_CurCustomTab = Tab;
377 }
378 }
379
380 auto LoadStartTime = time_get_nanoseconds();
381 SMenuAssetScanUser User;
382 User.m_pUser = this;
383 User.m_LoadedFunc = [&]() {
384 if(time_get_nanoseconds() - LoadStartTime > 500ms)
385 RenderLoading(pCaption: Localize(pStr: "Loading assets"), pContent: "", IncreaseCounter: 0);
386 };
387 if(s_CurCustomTab == ASSETS_TAB_ENTITIES)
388 {
389 if(m_vEntitiesList.empty())
390 {
391 SCustomEntities EntitiesItem;
392 str_copy(dst&: EntitiesItem.m_aName, src: "default");
393 LoadEntities(pEntitiesItem: &EntitiesItem, pUser: &User);
394 m_vEntitiesList.push_back(x: EntitiesItem);
395
396 // load entities
397 Storage()->ListDirectory(Type: IStorage::TYPE_ALL, pPath: "assets/entities", pfnCallback: EntitiesScan, pUser: &User);
398 std::sort(first: m_vEntitiesList.begin(), last: m_vEntitiesList.end());
399 }
400 if(m_vEntitiesList.size() != gs_aCustomListSize[s_CurCustomTab])
401 gs_aInitCustomList[s_CurCustomTab] = true;
402 }
403 else if(s_CurCustomTab == ASSETS_TAB_GAME)
404 {
405 InitAssetList(vAssetList&: m_vGameList, pAssetPath: "assets/game", pAssetName: "game", pfnCallback: GameScan, pGraphics: Graphics(), pStorage: Storage(), Caller: &User);
406 }
407 else if(s_CurCustomTab == ASSETS_TAB_EMOTICONS)
408 {
409 InitAssetList(vAssetList&: m_vEmoticonList, pAssetPath: "assets/emoticons", pAssetName: "emoticons", pfnCallback: EmoticonsScan, pGraphics: Graphics(), pStorage: Storage(), Caller: &User);
410 }
411 else if(s_CurCustomTab == ASSETS_TAB_PARTICLES)
412 {
413 InitAssetList(vAssetList&: m_vParticlesList, pAssetPath: "assets/particles", pAssetName: "particles", pfnCallback: ParticlesScan, pGraphics: Graphics(), pStorage: Storage(), Caller: &User);
414 }
415 else if(s_CurCustomTab == ASSETS_TAB_HUD)
416 {
417 InitAssetList(vAssetList&: m_vHudList, pAssetPath: "assets/hud", pAssetName: "hud", pfnCallback: HudScan, pGraphics: Graphics(), pStorage: Storage(), Caller: &User);
418 }
419 else if(s_CurCustomTab == ASSETS_TAB_EXTRAS)
420 {
421 InitAssetList(vAssetList&: m_vExtrasList, pAssetPath: "assets/extras", pAssetName: "extras", pfnCallback: ExtrasScan, pGraphics: Graphics(), pStorage: Storage(), Caller: &User);
422 }
423 else
424 {
425 dbg_assert_failed("Invalid s_CurCustomTab: %d", s_CurCustomTab);
426 }
427
428 MainView.HSplitTop(Cut: 10.0f, pTop: nullptr, pBottom: &MainView);
429
430 // skin selector
431 MainView.HSplitTop(Cut: MainView.h - 10.0f - ms_ButtonHeight, pTop: &CustomList, pBottom: &MainView);
432 if(gs_aInitCustomList[s_CurCustomTab])
433 {
434 int ListSize = 0;
435 if(s_CurCustomTab == ASSETS_TAB_ENTITIES)
436 {
437 gs_vpSearchEntitiesList.clear();
438 ListSize = m_vEntitiesList.size();
439 for(int i = 0; i < ListSize; ++i)
440 {
441 const SCustomEntities *pEntity = &m_vEntitiesList[i];
442
443 // filter quick search
444 if(!s_aFilterInputs[s_CurCustomTab].IsEmpty() && !str_utf8_find_nocase(haystack: pEntity->m_aName, needle: s_aFilterInputs[s_CurCustomTab].GetString()))
445 continue;
446
447 gs_vpSearchEntitiesList.push_back(x: pEntity);
448 }
449 }
450 else if(s_CurCustomTab == ASSETS_TAB_GAME)
451 {
452 ListSize = InitSearchList(vpSearchList&: gs_vpSearchGamesList, vAssetList&: m_vGameList);
453 }
454 else if(s_CurCustomTab == ASSETS_TAB_EMOTICONS)
455 {
456 ListSize = InitSearchList(vpSearchList&: gs_vpSearchEmoticonsList, vAssetList&: m_vEmoticonList);
457 }
458 else if(s_CurCustomTab == ASSETS_TAB_PARTICLES)
459 {
460 ListSize = InitSearchList(vpSearchList&: gs_vpSearchParticlesList, vAssetList&: m_vParticlesList);
461 }
462 else if(s_CurCustomTab == ASSETS_TAB_HUD)
463 {
464 ListSize = InitSearchList(vpSearchList&: gs_vpSearchHudList, vAssetList&: m_vHudList);
465 }
466 else if(s_CurCustomTab == ASSETS_TAB_EXTRAS)
467 {
468 ListSize = InitSearchList(vpSearchList&: gs_vpSearchExtrasList, vAssetList&: m_vExtrasList);
469 }
470 gs_aInitCustomList[s_CurCustomTab] = false;
471 gs_aCustomListSize[s_CurCustomTab] = ListSize;
472 }
473
474 int OldSelected = -1;
475 float Margin = 10;
476 float TextureWidth = 150;
477 float TextureHeight = 150;
478
479 size_t SearchListSize = 0;
480
481 if(s_CurCustomTab == ASSETS_TAB_ENTITIES)
482 {
483 SearchListSize = gs_vpSearchEntitiesList.size();
484 }
485 else if(s_CurCustomTab == ASSETS_TAB_GAME)
486 {
487 SearchListSize = gs_vpSearchGamesList.size();
488 TextureHeight = 75;
489 }
490 else if(s_CurCustomTab == ASSETS_TAB_EMOTICONS)
491 {
492 SearchListSize = gs_vpSearchEmoticonsList.size();
493 }
494 else if(s_CurCustomTab == ASSETS_TAB_PARTICLES)
495 {
496 SearchListSize = gs_vpSearchParticlesList.size();
497 }
498 else if(s_CurCustomTab == ASSETS_TAB_HUD)
499 {
500 SearchListSize = gs_vpSearchHudList.size();
501 }
502 else if(s_CurCustomTab == ASSETS_TAB_EXTRAS)
503 {
504 SearchListSize = gs_vpSearchExtrasList.size();
505 }
506
507 static CListBox s_ListBox;
508 s_ListBox.DoStart(RowHeight: TextureHeight + 15.0f + 10.0f + Margin, NumItems: SearchListSize, ItemsPerRow: CustomList.w / (Margin + TextureWidth), RowsPerScroll: 1, SelectedIndex: OldSelected, pRect: &CustomList, Background: false);
509 for(size_t i = 0; i < SearchListSize; ++i)
510 {
511 const SCustomItem *pItem = GetCustomItem(CurTab: s_CurCustomTab, Index: i);
512 if(pItem == nullptr)
513 continue;
514
515 if(s_CurCustomTab == ASSETS_TAB_ENTITIES)
516 {
517 if(str_comp(a: pItem->m_aName, b: g_Config.m_ClAssetsEntities) == 0)
518 OldSelected = i;
519 }
520 else if(s_CurCustomTab == ASSETS_TAB_GAME)
521 {
522 if(str_comp(a: pItem->m_aName, b: g_Config.m_ClAssetGame) == 0)
523 OldSelected = i;
524 }
525 else if(s_CurCustomTab == ASSETS_TAB_EMOTICONS)
526 {
527 if(str_comp(a: pItem->m_aName, b: g_Config.m_ClAssetEmoticons) == 0)
528 OldSelected = i;
529 }
530 else if(s_CurCustomTab == ASSETS_TAB_PARTICLES)
531 {
532 if(str_comp(a: pItem->m_aName, b: g_Config.m_ClAssetParticles) == 0)
533 OldSelected = i;
534 }
535 else if(s_CurCustomTab == ASSETS_TAB_HUD)
536 {
537 if(str_comp(a: pItem->m_aName, b: g_Config.m_ClAssetHud) == 0)
538 OldSelected = i;
539 }
540 else if(s_CurCustomTab == ASSETS_TAB_EXTRAS)
541 {
542 if(str_comp(a: pItem->m_aName, b: g_Config.m_ClAssetExtras) == 0)
543 OldSelected = i;
544 }
545
546 const CListboxItem Item = s_ListBox.DoNextItem(pId: pItem, Selected: OldSelected >= 0 && (size_t)OldSelected == i);
547 CUIRect ItemRect = Item.m_Rect;
548 ItemRect.Margin(Cut: Margin / 2, pOtherRect: &ItemRect);
549 if(!Item.m_Visible)
550 continue;
551
552 CUIRect TextureRect;
553 ItemRect.HSplitTop(Cut: 15, pTop: &ItemRect, pBottom: &TextureRect);
554 TextureRect.HSplitTop(Cut: 10, pTop: nullptr, pBottom: &TextureRect);
555 Ui()->DoLabel(pRect: &ItemRect, pText: pItem->m_aName, Size: ItemRect.h - 2, Align: TEXTALIGN_MC);
556 if(pItem->m_RenderTexture.IsValid())
557 {
558 Graphics()->WrapClamp();
559 Graphics()->TextureSet(Texture: pItem->m_RenderTexture);
560 Graphics()->QuadsBegin();
561 Graphics()->SetColor(r: 1, g: 1, b: 1, a: 1);
562 IGraphics::CQuadItem QuadItem(TextureRect.x + (TextureRect.w - TextureWidth) / 2, TextureRect.y + (TextureRect.h - TextureHeight) / 2, TextureWidth, TextureHeight);
563 Graphics()->QuadsDrawTL(pArray: &QuadItem, Num: 1);
564 Graphics()->QuadsEnd();
565 Graphics()->WrapNormal();
566 }
567 }
568
569 const int NewSelected = s_ListBox.DoEnd();
570 if(OldSelected != NewSelected)
571 {
572 if(GetCustomItem(CurTab: s_CurCustomTab, Index: NewSelected)->m_aName[0] != '\0')
573 {
574 if(s_CurCustomTab == ASSETS_TAB_ENTITIES)
575 {
576 str_copy(dst&: g_Config.m_ClAssetsEntities, src: GetCustomItem(CurTab: s_CurCustomTab, Index: NewSelected)->m_aName);
577 GameClient()->m_MapImages.ChangeEntitiesPath(pPath: GetCustomItem(CurTab: s_CurCustomTab, Index: NewSelected)->m_aName);
578 }
579 else if(s_CurCustomTab == ASSETS_TAB_GAME)
580 {
581 str_copy(dst&: g_Config.m_ClAssetGame, src: GetCustomItem(CurTab: s_CurCustomTab, Index: NewSelected)->m_aName);
582 GameClient()->LoadGameSkin(pPath: g_Config.m_ClAssetGame);
583 }
584 else if(s_CurCustomTab == ASSETS_TAB_EMOTICONS)
585 {
586 str_copy(dst&: g_Config.m_ClAssetEmoticons, src: GetCustomItem(CurTab: s_CurCustomTab, Index: NewSelected)->m_aName);
587 GameClient()->LoadEmoticonsSkin(pPath: g_Config.m_ClAssetEmoticons);
588 }
589 else if(s_CurCustomTab == ASSETS_TAB_PARTICLES)
590 {
591 str_copy(dst&: g_Config.m_ClAssetParticles, src: GetCustomItem(CurTab: s_CurCustomTab, Index: NewSelected)->m_aName);
592 GameClient()->LoadParticlesSkin(pPath: g_Config.m_ClAssetParticles);
593 }
594 else if(s_CurCustomTab == ASSETS_TAB_HUD)
595 {
596 str_copy(dst&: g_Config.m_ClAssetHud, src: GetCustomItem(CurTab: s_CurCustomTab, Index: NewSelected)->m_aName);
597 GameClient()->LoadHudSkin(pPath: g_Config.m_ClAssetHud);
598 }
599 else if(s_CurCustomTab == ASSETS_TAB_EXTRAS)
600 {
601 str_copy(dst&: g_Config.m_ClAssetExtras, src: GetCustomItem(CurTab: s_CurCustomTab, Index: NewSelected)->m_aName);
602 GameClient()->LoadExtrasSkin(pPath: g_Config.m_ClAssetExtras);
603 }
604 }
605 }
606
607 // Quick search
608 MainView.HSplitBottom(Cut: ms_ButtonHeight, pTop: &MainView, pBottom: &QuickSearch);
609 QuickSearch.VSplitLeft(Cut: 220.0f, pLeft: &QuickSearch, pRight: &DirectoryButton);
610 QuickSearch.HSplitTop(Cut: 5.0f, pTop: nullptr, pBottom: &QuickSearch);
611 if(Ui()->DoEditBox_Search(pLineInput: &s_aFilterInputs[s_CurCustomTab], pRect: &QuickSearch, FontSize: 14.0f, HotkeyEnabled: !Ui()->IsPopupOpen() && !GameClient()->m_GameConsole.IsActive()))
612 {
613 gs_aInitCustomList[s_CurCustomTab] = true;
614 }
615
616 DirectoryButton.HSplitTop(Cut: 5.0f, pTop: nullptr, pBottom: &DirectoryButton);
617 DirectoryButton.VSplitRight(Cut: 175.0f, pLeft: nullptr, pRight: &DirectoryButton);
618 DirectoryButton.VSplitRight(Cut: 25.0f, pLeft: &DirectoryButton, pRight: &ReloadButton);
619 DirectoryButton.VSplitRight(Cut: 10.0f, pLeft: &DirectoryButton, pRight: nullptr);
620 static CButtonContainer s_AssetsDirId;
621 if(DoButton_Menu(pButtonContainer: &s_AssetsDirId, pText: Localize(pStr: "Assets directory"), Checked: 0, pRect: &DirectoryButton))
622 {
623 char aBuf[IO_MAX_PATH_LENGTH];
624 char aBufFull[IO_MAX_PATH_LENGTH + 7];
625 if(s_CurCustomTab == ASSETS_TAB_ENTITIES)
626 str_copy(dst&: aBufFull, src: "assets/entities");
627 else if(s_CurCustomTab == ASSETS_TAB_GAME)
628 str_copy(dst&: aBufFull, src: "assets/game");
629 else if(s_CurCustomTab == ASSETS_TAB_EMOTICONS)
630 str_copy(dst&: aBufFull, src: "assets/emoticons");
631 else if(s_CurCustomTab == ASSETS_TAB_PARTICLES)
632 str_copy(dst&: aBufFull, src: "assets/particles");
633 else if(s_CurCustomTab == ASSETS_TAB_HUD)
634 str_copy(dst&: aBufFull, src: "assets/hud");
635 else if(s_CurCustomTab == ASSETS_TAB_EXTRAS)
636 str_copy(dst&: aBufFull, src: "assets/extras");
637 Storage()->GetCompletePath(Type: IStorage::TYPE_SAVE, pDir: aBufFull, pBuffer: aBuf, BufferSize: sizeof(aBuf));
638 Storage()->CreateFolder(pFoldername: "assets", Type: IStorage::TYPE_SAVE);
639 Storage()->CreateFolder(pFoldername: aBufFull, Type: IStorage::TYPE_SAVE);
640 Client()->ViewFile(pFilename: aBuf);
641 }
642 GameClient()->m_Tooltips.DoToolTip(pId: &s_AssetsDirId, pNearRect: &DirectoryButton, pText: Localize(pStr: "Open the directory to add custom assets"));
643
644 TextRender()->SetFontPreset(EFontPreset::ICON_FONT);
645 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);
646 static CButtonContainer s_AssetsReloadBtnId;
647 if(DoButton_Menu(pButtonContainer: &s_AssetsReloadBtnId, pText: FontIcon::ARROW_ROTATE_RIGHT, Checked: 0, pRect: &ReloadButton) || Input()->KeyPress(Key: KEY_F5) || (Input()->KeyPress(Key: KEY_R) && Input()->ModifierIsPressed()))
648 {
649 ClearCustomItems(CurTab: s_CurCustomTab);
650 }
651 TextRender()->SetRenderFlags(0);
652 TextRender()->SetFontPreset(EFontPreset::DEFAULT_FONT);
653}
654
655void CMenus::ConchainAssetsEntities(IConsole::IResult *pResult, void *pUserData, IConsole::FCommandCallback pfnCallback, void *pCallbackUserData)
656{
657 CMenus *pThis = (CMenus *)pUserData;
658 if(pResult->NumArguments() == 1)
659 {
660 const char *pArg = pResult->GetString(Index: 0);
661 if(str_comp(a: pArg, b: g_Config.m_ClAssetsEntities) != 0)
662 {
663 pThis->GameClient()->m_MapImages.ChangeEntitiesPath(pPath: pArg);
664 }
665 }
666
667 pfnCallback(pResult, pCallbackUserData);
668}
669
670void CMenus::ConchainAssetGame(IConsole::IResult *pResult, void *pUserData, IConsole::FCommandCallback pfnCallback, void *pCallbackUserData)
671{
672 CMenus *pThis = (CMenus *)pUserData;
673 if(pResult->NumArguments() == 1)
674 {
675 const char *pArg = pResult->GetString(Index: 0);
676 if(str_comp(a: pArg, b: g_Config.m_ClAssetGame) != 0)
677 {
678 pThis->GameClient()->LoadGameSkin(pPath: pArg);
679 }
680 }
681
682 pfnCallback(pResult, pCallbackUserData);
683}
684
685void CMenus::ConchainAssetParticles(IConsole::IResult *pResult, void *pUserData, IConsole::FCommandCallback pfnCallback, void *pCallbackUserData)
686{
687 CMenus *pThis = (CMenus *)pUserData;
688 if(pResult->NumArguments() == 1)
689 {
690 const char *pArg = pResult->GetString(Index: 0);
691 if(str_comp(a: pArg, b: g_Config.m_ClAssetParticles) != 0)
692 {
693 pThis->GameClient()->LoadParticlesSkin(pPath: pArg);
694 }
695 }
696
697 pfnCallback(pResult, pCallbackUserData);
698}
699
700void CMenus::ConchainAssetEmoticons(IConsole::IResult *pResult, void *pUserData, IConsole::FCommandCallback pfnCallback, void *pCallbackUserData)
701{
702 CMenus *pThis = (CMenus *)pUserData;
703 if(pResult->NumArguments() == 1)
704 {
705 const char *pArg = pResult->GetString(Index: 0);
706 if(str_comp(a: pArg, b: g_Config.m_ClAssetEmoticons) != 0)
707 {
708 pThis->GameClient()->LoadEmoticonsSkin(pPath: pArg);
709 }
710 }
711
712 pfnCallback(pResult, pCallbackUserData);
713}
714
715void CMenus::ConchainAssetHud(IConsole::IResult *pResult, void *pUserData, IConsole::FCommandCallback pfnCallback, void *pCallbackUserData)
716{
717 CMenus *pThis = (CMenus *)pUserData;
718 if(pResult->NumArguments() == 1)
719 {
720 const char *pArg = pResult->GetString(Index: 0);
721 if(str_comp(a: pArg, b: g_Config.m_ClAssetHud) != 0)
722 {
723 pThis->GameClient()->LoadHudSkin(pPath: pArg);
724 }
725 }
726
727 pfnCallback(pResult, pCallbackUserData);
728}
729
730void CMenus::ConchainAssetExtras(IConsole::IResult *pResult, void *pUserData, IConsole::FCommandCallback pfnCallback, void *pCallbackUserData)
731{
732 CMenus *pThis = (CMenus *)pUserData;
733 if(pResult->NumArguments() == 1)
734 {
735 const char *pArg = pResult->GetString(Index: 0);
736 if(str_comp(a: pArg, b: g_Config.m_ClAssetExtras) != 0)
737 {
738 pThis->GameClient()->LoadExtrasSkin(pPath: pArg);
739 }
740 }
741
742 pfnCallback(pResult, pCallbackUserData);
743}
744