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