1#include <base/system.h>
2
3#include <algorithm>
4
5#include <engine/graphics.h>
6#include <engine/map.h>
7#include <engine/shared/config.h>
8
9#include <game/client/components/camera.h>
10#include <game/client/components/mapimages.h>
11#include <game/client/components/maplayers.h>
12#include <game/client/gameclient.h>
13
14#include <game/layers.h>
15#include <game/localization.h>
16#include <game/mapitems.h>
17
18#include "menu_background.h"
19
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(CMapLayers::TYPE_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}
70
71CBackgroundEngineMap *CMenuBackground::CreateBGMap()
72{
73 return new CMenuMap;
74}
75
76void CMenuBackground::OnInit()
77{
78 m_pBackgroundMap = CreateBGMap();
79 m_pMap = m_pBackgroundMap;
80
81 m_IsInit = true;
82
83 m_pImages->m_pClient = GameClient();
84 Kernel()->RegisterInterface<CMenuMap>(pInterface: (CMenuMap *)m_pBackgroundMap);
85 if(g_Config.m_ClMenuMap[0] != '\0')
86 LoadMenuBackground();
87
88 m_Camera.m_pClient = GameClient();
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, RenderLoadingBar: false);
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)
173 m_pMap->Unload();
174
175 m_Loaded = false;
176 m_pMap = m_pBackgroundMap;
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 const char *pMenuMap = g_Config.m_ClMenuMap;
187 if(str_comp(a: pMenuMap, b: "auto") == 0)
188 {
189 switch(time_season())
190 {
191 case SEASON_SPRING:
192 case SEASON_EASTER:
193 pMenuMap = "heavens";
194 break;
195 case SEASON_SUMMER:
196 pMenuMap = "jungle";
197 break;
198 case SEASON_AUTUMN:
199 case SEASON_HALLOWEEN:
200 pMenuMap = "autumn";
201 break;
202 case SEASON_WINTER:
203 case SEASON_XMAS:
204 pMenuMap = "winter";
205 break;
206 case SEASON_NEWYEAR:
207 pMenuMap = "newyear";
208 break;
209 }
210 }
211 else if(str_comp(a: pMenuMap, b: "rand") == 0)
212 {
213 // make sure to load themes
214 const std::vector<CTheme> &vThemesRef = GetThemes();
215 if(vThemesRef.size() > PREDEFINED_THEMES_COUNT)
216 {
217 int RandomThemeIndex = rand() % (vThemesRef.size() - PREDEFINED_THEMES_COUNT);
218 if(RandomThemeIndex + PREDEFINED_THEMES_COUNT < (int)vThemesRef.size())
219 pMenuMap = vThemesRef[RandomThemeIndex + PREDEFINED_THEMES_COUNT].m_Name.c_str();
220 }
221 }
222
223 char aBuf[128];
224
225 const int HourOfTheDay = time_houroftheday();
226 const bool IsDaytime = HourOfTheDay >= 6 && HourOfTheDay < 18;
227
228 if(!m_Loaded && ((HasDayHint && IsDaytime) || (HasNightHint && !IsDaytime)))
229 {
230 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "themes/%s_%s.map", pMenuMap, IsDaytime ? "day" : "night");
231 if(m_pMap->Load(pMapName: aBuf))
232 {
233 m_Loaded = true;
234 }
235 }
236
237 if(!m_Loaded)
238 {
239 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "themes/%s.map", pMenuMap);
240 if(m_pMap->Load(pMapName: aBuf))
241 {
242 m_Loaded = true;
243 }
244 }
245
246 if(!m_Loaded && ((HasDayHint && !IsDaytime) || (HasNightHint && IsDaytime)))
247 {
248 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "themes/%s_%s.map", pMenuMap, IsDaytime ? "night" : "day");
249 if(m_pMap->Load(pMapName: aBuf))
250 {
251 m_Loaded = true;
252 }
253 }
254
255 if(m_Loaded)
256 {
257 m_pLayers->InitBackground(pMap: m_pMap);
258
259 CMapLayers::OnMapLoad();
260 m_pImages->LoadBackground(pLayers: m_pLayers, pMap: m_pMap);
261
262 // look for custom positions
263 CMapItemLayerTilemap *pTLayer = m_pLayers->GameLayer();
264 if(pTLayer)
265 {
266 int DataIndex = pTLayer->m_Data;
267 unsigned int Size = m_pLayers->Map()->GetDataSize(Index: DataIndex);
268 void *pTiles = m_pLayers->Map()->GetData(Index: DataIndex);
269 unsigned int TileSize = sizeof(CTile);
270
271 if(Size >= pTLayer->m_Width * pTLayer->m_Height * TileSize)
272 {
273 for(int y = 0; y < pTLayer->m_Height; ++y)
274 {
275 for(int x = 0; x < pTLayer->m_Width; ++x)
276 {
277 unsigned char Index = ((CTile *)pTiles)[y * pTLayer->m_Width + x].m_Index;
278 if(Index >= TILE_TIME_CHECKPOINT_FIRST && Index <= TILE_TIME_CHECKPOINT_LAST)
279 {
280 int ArrayIndex = clamp<int>(val: (Index - TILE_TIME_CHECKPOINT_FIRST), lo: 0, hi: NUM_POS);
281 m_aPositions[ArrayIndex] = vec2(x * 32.0f + 16.0f, y * 32.0f + 16.0f);
282 }
283
284 x += ((CTile *)pTiles)[y * pTLayer->m_Width + x].m_Skip;
285 }
286 }
287 }
288 }
289 }
290 }
291}
292
293void CMenuBackground::OnMapLoad()
294{
295}
296
297void CMenuBackground::OnRender()
298{
299}
300
301bool CMenuBackground::Render()
302{
303 if(!m_Loaded)
304 return false;
305
306 if(Client()->State() == IClient::STATE_ONLINE || Client()->State() == IClient::STATE_DEMOPLAYBACK)
307 return false;
308
309 m_Camera.m_Zoom = 0.7f;
310 static vec2 Dir = vec2(1.0f, 0.0f);
311
312 float DistToCenter = distance(a: m_Camera.m_Center, b: m_RotationCenter);
313 if(!m_ChangedPosition && absolute(a: DistToCenter - (float)g_Config.m_ClRotationRadius) <= 0.5f)
314 {
315 // do little rotation
316 float RotPerTick = 360.0f / (float)g_Config.m_ClRotationSpeed * clamp(val: Client()->RenderFrameTime(), lo: 0.0f, hi: 0.1f);
317 Dir = rotate(a: Dir, angle: RotPerTick);
318 m_Camera.m_Center = m_RotationCenter + Dir * (float)g_Config.m_ClRotationRadius;
319 }
320 else
321 {
322 // positions for the animation
323 vec2 DirToCenter;
324 if(DistToCenter > 0.5f)
325 DirToCenter = normalize(v: m_AnimationStartPos - m_RotationCenter);
326 else
327 DirToCenter = vec2(1, 0);
328 vec2 TargetPos = m_RotationCenter + DirToCenter * (float)g_Config.m_ClRotationRadius;
329 float Distance = distance(a: m_AnimationStartPos, b: TargetPos);
330 if(Distance > 0.001f)
331 Dir = normalize(v: m_AnimationStartPos - TargetPos);
332 else
333 Dir = vec2(1, 0);
334
335 // move time
336 m_MoveTime += clamp(val: Client()->RenderFrameTime(), lo: 0.0f, hi: 0.1f) * g_Config.m_ClCameraSpeed / 10.0f;
337 float XVal = 1 - m_MoveTime;
338 XVal = std::pow(x: XVal, y: 7.0f);
339
340 m_Camera.m_Center = TargetPos + Dir * (XVal * Distance);
341 if(m_CurrentPosition < 0)
342 {
343 m_AnimationStartPos = m_Camera.m_Center;
344 m_MoveTime = 0.0f;
345 }
346
347 m_ChangedPosition = false;
348 }
349
350 CMapLayers::OnRender();
351
352 m_CurrentPosition = -1;
353
354 return true;
355}
356
357CCamera *CMenuBackground::GetCurCamera()
358{
359 return &m_Camera;
360}
361
362void CMenuBackground::ChangePosition(int PositionNumber)
363{
364 if(PositionNumber != m_CurrentPosition)
365 {
366 if(PositionNumber >= POS_START && PositionNumber < NUM_POS)
367 {
368 m_CurrentPosition = PositionNumber;
369 }
370 else
371 {
372 m_CurrentPosition = POS_START;
373 }
374
375 m_ChangedPosition = true;
376 }
377 m_AnimationStartPos = m_Camera.m_Center;
378 m_RotationCenter = m_aPositions[m_CurrentPosition];
379 m_MoveTime = 0.0f;
380}
381
382std::vector<CTheme> &CMenuBackground::GetThemes()
383{
384 if(m_vThemes.empty()) // not loaded yet
385 {
386 // when adding more here, make sure to change the value of PREDEFINED_THEMES_COUNT too
387 m_vThemes.emplace_back(args: "", args: true, args: true); // no theme
388 LoadThemeIcon(Theme&: m_vThemes.back());
389
390 m_vThemes.emplace_back(args: "auto", args: true, args: true); // auto theme
391 LoadThemeIcon(Theme&: m_vThemes.back());
392
393 m_vThemes.emplace_back(args: "rand", args: true, args: true); // random theme
394 LoadThemeIcon(Theme&: m_vThemes.back());
395
396 m_ThemeScanStartTime = time_get_nanoseconds();
397 Storage()->ListDirectory(Type: IStorage::TYPE_ALL, pPath: "themes", pfnCallback: ThemeScan, pUser: this);
398
399 std::sort(first: m_vThemes.begin() + PREDEFINED_THEMES_COUNT, last: m_vThemes.end());
400 }
401 return m_vThemes;
402}
403