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