1#include "menu_background.h"
2
3#include <base/dbg.h>
4#include <base/str.h>
5#include <base/time.h>
6
7#include <engine/graphics.h>
8#include <engine/map.h>
9#include <engine/shared/config.h>
10
11#include <game/client/components/camera.h>
12#include <game/client/components/mapimages.h>
13#include <game/client/components/maplayers.h>
14#include <game/client/gameclient.h>
15#include <game/layers.h>
16#include <game/localization.h>
17#include <game/mapitems.h>
18
19#include <algorithm>
20#include <chrono>
21
22using namespace std::chrono_literals;
23
24std::array<vec2, CMenuBackground::NUM_POS> GenerateMenuBackgroundPositions()
25{
26 std::array<vec2, CMenuBackground::NUM_POS> Positions;
27
28 Positions[CMenuBackground::POS_START] = vec2(500.0f, 500.0f);
29 Positions[CMenuBackground::POS_BROWSER_INTERNET] = vec2(1000.0f, 1000.0f);
30 Positions[CMenuBackground::POS_BROWSER_LAN] = vec2(1100.0f, 1000.0f);
31 Positions[CMenuBackground::POS_DEMOS] = vec2(900.0f, 100.0f);
32 Positions[CMenuBackground::POS_NEWS] = vec2(500.0f, 750.0f);
33 Positions[CMenuBackground::POS_BROWSER_FAVORITES] = vec2(1250.0f, 500.0f);
34 Positions[CMenuBackground::POS_SETTINGS_LANGUAGE] = vec2(500.0f, 1200.0f);
35 Positions[CMenuBackground::POS_SETTINGS_GENERAL] = vec2(500.0f, 1000.0f);
36 Positions[CMenuBackground::POS_SETTINGS_PLAYER] = vec2(600.0f, 1000.0f);
37 Positions[CMenuBackground::POS_SETTINGS_TEE] = vec2(700.0f, 1000.0f);
38 Positions[CMenuBackground::POS_SETTINGS_APPEARANCE] = vec2(200.0f, 1000.0f);
39 Positions[CMenuBackground::POS_SETTINGS_CONTROLS] = vec2(800.0f, 1000.0f);
40 Positions[CMenuBackground::POS_SETTINGS_GRAPHICS] = vec2(900.0f, 1000.0f);
41 Positions[CMenuBackground::POS_SETTINGS_SOUND] = vec2(1000.0f, 1000.0f);
42 Positions[CMenuBackground::POS_SETTINGS_DDNET] = vec2(1200.0f, 200.0f);
43 Positions[CMenuBackground::POS_SETTINGS_ASSETS] = vec2(500.0f, 500.0f);
44 for(int i = 0; i < CMenuBackground::POS_BROWSER_CUSTOM_NUM; ++i)
45 Positions[CMenuBackground::POS_BROWSER_CUSTOM0 + i] = vec2(500.0f + (75.0f * (float)i), 650.0f - (75.0f * (float)i));
46 for(int i = 0; i < CMenuBackground::POS_SETTINGS_RESERVED_NUM; ++i)
47 Positions[CMenuBackground::POS_SETTINGS_RESERVED0 + i] = vec2(0, 0);
48 for(int i = 0; i < CMenuBackground::POS_RESERVED_NUM; ++i)
49 Positions[CMenuBackground::POS_RESERVED0 + i] = vec2(0, 0);
50
51 return Positions;
52}
53
54CMenuBackground::CMenuBackground() :
55 CBackground(ERenderType::RENDERTYPE_FULL_DESIGN, false)
56{
57 m_RotationCenter = vec2(0.0f, 0.0f);
58 m_AnimationStartPos = vec2(0.0f, 0.0f);
59 m_Camera.m_Center = vec2(0.0f, 0.0f);
60 m_Camera.m_PrevCenter = vec2(0.0f, 0.0f); // unused in this class
61 m_ChangedPosition = false;
62
63 ResetPositions();
64
65 m_CurrentPosition = -1;
66 m_MoveTime = 0.0f;
67
68 m_IsInit = false;
69 m_Loading = false;
70}
71
72void CMenuBackground::OnInterfacesInit(CGameClient *pClient)
73{
74 CComponentInterfaces::OnInterfacesInit(pClient);
75 m_pImages->OnInterfacesInit(pClient);
76 m_Camera.OnInterfacesInit(pClient);
77}
78
79void CMenuBackground::OnInit()
80{
81 m_pBackgroundMap = CreateMap();
82 m_pMap = m_pBackgroundMap.get();
83
84 m_IsInit = true;
85
86 if(g_Config.m_ClMenuMap[0] != '\0')
87 LoadMenuBackground();
88
89 m_Camera.m_ZoomSet = false;
90 m_Camera.m_ZoomSmoothingTarget = 0;
91}
92
93void CMenuBackground::ResetPositions()
94{
95 m_aPositions = GenerateMenuBackgroundPositions();
96}
97
98void CMenuBackground::LoadThemeIcon(CTheme &Theme)
99{
100 char aIconPath[IO_MAX_PATH_LENGTH];
101 str_format(buffer: aIconPath, buffer_size: sizeof(aIconPath), format: "themes/%s.png", Theme.m_Name.empty() ? "none" : Theme.m_Name.c_str());
102 Theme.m_IconTexture = Graphics()->LoadTexture(pFilename: aIconPath, StorageType: IStorage::TYPE_ALL);
103
104 char aBuf[32 + IO_MAX_PATH_LENGTH];
105 if(Theme.m_IconTexture.IsNullTexture())
106 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "failed to load theme icon '%s'", aIconPath);
107 else
108 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "loaded theme icon '%s'", aIconPath);
109 Console()->Print(Level: IConsole::OUTPUT_LEVEL_ADDINFO, pFrom: "menuthemes", pStr: aBuf);
110}
111
112int CMenuBackground::ThemeScan(const char *pName, int IsDir, int DirType, void *pUser)
113{
114 CMenuBackground *pSelf = (CMenuBackground *)pUser;
115 const char *pSuffix = str_endswith(str: pName, suffix: ".map");
116 if(IsDir || !pSuffix)
117 return 0;
118 char aFullName[128];
119 char aThemeName[128];
120 str_truncate(dst: aFullName, dst_size: sizeof(aFullName), src: pName, truncation_len: pSuffix - pName);
121
122 bool IsDay = false;
123 bool IsNight = false;
124 if((pSuffix = str_endswith(str: aFullName, suffix: "_day")))
125 {
126 str_truncate(dst: aThemeName, dst_size: sizeof(aThemeName), src: pName, truncation_len: pSuffix - aFullName);
127 IsDay = true;
128 }
129 else if((pSuffix = str_endswith(str: aFullName, suffix: "_night")))
130 {
131 str_truncate(dst: aThemeName, dst_size: sizeof(aThemeName), src: pName, truncation_len: pSuffix - aFullName);
132 IsNight = true;
133 }
134 else
135 str_copy(dst&: aThemeName, src: aFullName);
136
137 if(str_comp(a: aThemeName, b: "none") == 0 || str_comp(a: aThemeName, b: "auto") == 0 || str_comp(a: aThemeName, b: "rand") == 0) // "none", "auto" and "rand" reserved, disallowed for maps
138 return 0;
139
140 // try to edit an existing theme
141 for(auto &Theme : pSelf->m_vThemes)
142 {
143 if(str_comp(a: Theme.m_Name.c_str(), b: aThemeName) == 0)
144 {
145 if(IsDay)
146 Theme.m_HasDay = true;
147 if(IsNight)
148 Theme.m_HasNight = true;
149 return 0;
150 }
151 }
152
153 // make new theme
154 char aBuf[512];
155 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "added theme '%s' from 'themes/%s'", aThemeName, pName);
156 pSelf->Console()->Print(Level: IConsole::OUTPUT_LEVEL_ADDINFO, pFrom: "menuthemes", pStr: aBuf);
157 pSelf->m_vThemes.emplace_back(args&: aThemeName, args&: IsDay, args&: IsNight);
158 pSelf->LoadThemeIcon(Theme&: pSelf->m_vThemes.back());
159
160 if(time_get_nanoseconds() - pSelf->m_ThemeScanStartTime > 500ms)
161 {
162 pSelf->GameClient()->m_Menus.RenderLoading(pCaption: Localize(pStr: "Loading menu themes"), pContent: "", IncreaseCounter: 0);
163 }
164 return 0;
165}
166
167void CMenuBackground::LoadMenuBackground(bool HasDayHint, bool HasNightHint)
168{
169 if(!m_IsInit)
170 return;
171
172 if(m_Loaded && m_pMap == m_pBackgroundMap.get())
173 m_pMap->Unload();
174
175 m_Loaded = false;
176 m_pMap = m_pBackgroundMap.get();
177 m_pLayers = m_pBackgroundLayers;
178 m_pImages = m_pBackgroundImages;
179
180 ResetPositions();
181
182 str_copy(dst&: m_aMapName, src: g_Config.m_ClMenuMap);
183
184 if(g_Config.m_ClMenuMap[0] != '\0')
185 {
186 m_Loading = true;
187
188 const char *pMenuMap = g_Config.m_ClMenuMap;
189 if(str_comp(a: pMenuMap, b: "auto") == 0)
190 {
191 const ETimeSeason Season = time_season();
192 switch(Season)
193 {
194 case ETimeSeason::SPRING:
195 case ETimeSeason::EASTER:
196 pMenuMap = "heavens";
197 break;
198 case ETimeSeason::SUMMER:
199 pMenuMap = "jungle";
200 break;
201 case ETimeSeason::AUTUMN:
202 case ETimeSeason::HALLOWEEN:
203 pMenuMap = "autumn";
204 break;
205 case ETimeSeason::WINTER:
206 case ETimeSeason::XMAS:
207 pMenuMap = "winter";
208 break;
209 case ETimeSeason::NEWYEAR:
210 pMenuMap = "newyear";
211 break;
212 default:
213 dbg_assert_failed("Invalid season: %d", (int)Season);
214 }
215 }
216 else if(str_comp(a: pMenuMap, b: "rand") == 0)
217 {
218 // make sure to load themes
219 const std::vector<CTheme> &vThemesRef = GetThemes();
220 if(vThemesRef.size() > PREDEFINED_THEMES_COUNT)
221 {
222 int RandomThemeIndex = rand() % (vThemesRef.size() - PREDEFINED_THEMES_COUNT);
223 if(RandomThemeIndex + PREDEFINED_THEMES_COUNT < (int)vThemesRef.size())
224 pMenuMap = vThemesRef[RandomThemeIndex + PREDEFINED_THEMES_COUNT].m_Name.c_str();
225 }
226 }
227
228 char aBuf[128];
229
230 const int HourOfTheDay = time_houroftheday();
231 const bool IsDaytime = HourOfTheDay >= 6 && HourOfTheDay < 18;
232
233 if(!m_Loaded && ((HasDayHint && IsDaytime) || (HasNightHint && !IsDaytime)))
234 {
235 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "themes/%s_%s.map", pMenuMap, IsDaytime ? "day" : "night");
236 if(m_pMap->Load(pFullName: pMenuMap, pStorage: Storage(), pPath: aBuf, StorageType: IStorage::TYPE_ALL))
237 {
238 m_Loaded = true;
239 }
240 }
241
242 if(!m_Loaded)
243 {
244 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "themes/%s.map", pMenuMap);
245 if(m_pMap->Load(pFullName: pMenuMap, pStorage: Storage(), pPath: aBuf, StorageType: IStorage::TYPE_ALL))
246 {
247 m_Loaded = true;
248 }
249 }
250
251 if(!m_Loaded && ((HasDayHint && !IsDaytime) || (HasNightHint && IsDaytime)))
252 {
253 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "themes/%s_%s.map", pMenuMap, IsDaytime ? "night" : "day");
254 if(m_pMap->Load(pFullName: pMenuMap, pStorage: Storage(), pPath: aBuf, StorageType: IStorage::TYPE_ALL))
255 {
256 m_Loaded = true;
257 }
258 }
259
260 if(m_Loaded)
261 {
262 m_pLayers->Init(pMap: m_pMap, GameOnly: true);
263
264 m_pImages->LoadBackground(pLayers: m_pLayers, pMap: m_pMap);
265 CMapLayers::OnMapLoad();
266
267 // look for custom positions
268 CMapItemLayerTilemap *pTLayer = m_pLayers->GameLayer();
269 if(pTLayer)
270 {
271 int DataIndex = pTLayer->m_Data;
272 unsigned int Size = m_pLayers->Map()->GetDataSize(Index: DataIndex);
273 void *pTiles = m_pLayers->Map()->GetData(Index: DataIndex);
274 unsigned int TileSize = sizeof(CTile);
275
276 if(Size >= pTLayer->m_Width * pTLayer->m_Height * TileSize)
277 {
278 for(int y = 0; y < pTLayer->m_Height; ++y)
279 {
280 for(int x = 0; x < pTLayer->m_Width; ++x)
281 {
282 unsigned char Index = ((CTile *)pTiles)[y * pTLayer->m_Width + x].m_Index;
283 if(Index >= TILE_TIME_CHECKPOINT_FIRST && Index <= TILE_TIME_CHECKPOINT_LAST)
284 {
285 int ArrayIndex = std::clamp<int>(val: (Index - TILE_TIME_CHECKPOINT_FIRST), lo: 0, hi: NUM_POS);
286 m_aPositions[ArrayIndex] = vec2(x * 32.0f + 16.0f, y * 32.0f + 16.0f);
287 }
288
289 x += ((CTile *)pTiles)[y * pTLayer->m_Width + x].m_Skip;
290 }
291 }
292 }
293 }
294 }
295 m_Loading = false;
296 }
297}
298
299void CMenuBackground::OnMapLoad()
300{
301}
302
303void CMenuBackground::OnRender()
304{
305}
306
307bool CMenuBackground::Render()
308{
309 if(!m_Loaded)
310 return false;
311
312 m_Camera.m_Zoom = 0.7f;
313
314 float DistToCenter = distance(a: m_Camera.m_Center, b: m_RotationCenter);
315 if(!m_ChangedPosition && absolute(a: DistToCenter - (float)g_Config.m_ClRotationRadius) <= 0.5f)
316 {
317 // do little rotation
318 float RotPerTick = 360.0f / (float)g_Config.m_ClRotationSpeed * std::clamp(val: Client()->RenderFrameTime(), lo: 0.0f, hi: 0.1f);
319 m_CurrentDirection = rotate(a: m_CurrentDirection, angle: RotPerTick);
320 m_Camera.m_Center = m_RotationCenter + m_CurrentDirection * (float)g_Config.m_ClRotationRadius;
321 }
322 else
323 {
324 // positions for the animation
325 vec2 DirToCenter;
326 if(DistToCenter > 0.5f)
327 DirToCenter = normalize(v: m_AnimationStartPos - m_RotationCenter);
328 else
329 DirToCenter = vec2(1, 0);
330 vec2 TargetPos = m_RotationCenter + DirToCenter * (float)g_Config.m_ClRotationRadius;
331 float Distance = distance(a: m_AnimationStartPos, b: TargetPos);
332 if(Distance > 0.001f)
333 m_CurrentDirection = normalize(v: m_AnimationStartPos - TargetPos);
334 else
335 m_CurrentDirection = vec2(1.0f, 0.0f);
336
337 // move time
338 m_MoveTime += std::clamp(val: Client()->RenderFrameTime(), lo: 0.0f, hi: 0.1f) * g_Config.m_ClCameraSpeed / 10.0f;
339 float XVal = 1 - m_MoveTime;
340 XVal = std::pow(x: XVal, y: 7.0f);
341
342 m_Camera.m_Center = TargetPos + m_CurrentDirection * (XVal * Distance);
343 if(m_CurrentPosition < 0)
344 {
345 m_AnimationStartPos = m_Camera.m_Center;
346 m_MoveTime = 0.0f;
347 }
348
349 m_ChangedPosition = false;
350 }
351
352 CMapLayers::OnRender();
353
354 m_CurrentPosition = -1;
355
356 return true;
357}
358
359CCamera *CMenuBackground::GetCurCamera()
360{
361 return &m_Camera;
362}
363
364void CMenuBackground::ChangePosition(int PositionNumber)
365{
366 if(PositionNumber != m_CurrentPosition)
367 {
368 if(PositionNumber >= POS_START && PositionNumber < NUM_POS)
369 {
370 m_CurrentPosition = PositionNumber;
371 }
372 else
373 {
374 m_CurrentPosition = POS_START;
375 }
376
377 m_ChangedPosition = true;
378 }
379 m_AnimationStartPos = m_Camera.m_Center;
380 m_RotationCenter = m_aPositions[m_CurrentPosition];
381 m_MoveTime = 0.0f;
382}
383
384std::vector<CTheme> &CMenuBackground::GetThemes()
385{
386 if(m_vThemes.empty()) // not loaded yet
387 {
388 // when adding more here, make sure to change the value of PREDEFINED_THEMES_COUNT too
389 m_vThemes.emplace_back(args: "", args: true, args: true); // no theme
390 LoadThemeIcon(Theme&: m_vThemes.back());
391
392 m_vThemes.emplace_back(args: "auto", args: true, args: true); // auto theme
393 LoadThemeIcon(Theme&: m_vThemes.back());
394
395 m_vThemes.emplace_back(args: "rand", args: true, args: true); // random theme
396 LoadThemeIcon(Theme&: m_vThemes.back());
397
398 m_ThemeScanStartTime = time_get_nanoseconds();
399 Storage()->ListDirectory(Type: IStorage::TYPE_ALL, pPath: "themes", pfnCallback: ThemeScan, pUser: this);
400
401 std::sort(first: m_vThemes.begin() + PREDEFINED_THEMES_COUNT, last: m_vThemes.end());
402 }
403 return m_vThemes;
404}
405