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
70void CMenuBackground::OnInterfacesInit(CGameClient *pClient)
71{
72 CComponentInterfaces::OnInterfacesInit(pClient);
73 m_pImages->OnInterfacesInit(pClient);
74 m_Camera.OnInterfacesInit(pClient);
75}
76
77void CMenuBackground::OnInit()
78{
79 m_pBackgroundMap = CreateMap();
80 m_pMap = m_pBackgroundMap.get();
81
82 m_IsInit = true;
83
84 if(g_Config.m_ClMenuMap[0] != '\0')
85 LoadMenuBackground();
86
87 m_Camera.m_ZoomSet = false;
88 m_Camera.m_ZoomSmoothingTarget = 0;
89}
90
91void CMenuBackground::ResetPositions()
92{
93 m_aPositions = GenerateMenuBackgroundPositions();
94}
95
96void CMenuBackground::LoadThemeIcon(CTheme &Theme)
97{
98 char aIconPath[IO_MAX_PATH_LENGTH];
99 str_format(buffer: aIconPath, buffer_size: sizeof(aIconPath), format: "themes/%s.png", Theme.m_Name.empty() ? "none" : Theme.m_Name.c_str());
100 Theme.m_IconTexture = Graphics()->LoadTexture(pFilename: aIconPath, StorageType: IStorage::TYPE_ALL);
101
102 char aBuf[32 + IO_MAX_PATH_LENGTH];
103 if(Theme.m_IconTexture.IsNullTexture())
104 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "failed to load theme icon '%s'", aIconPath);
105 else
106 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "loaded theme icon '%s'", aIconPath);
107 Console()->Print(Level: IConsole::OUTPUT_LEVEL_ADDINFO, pFrom: "menuthemes", pStr: aBuf);
108}
109
110int CMenuBackground::ThemeScan(const char *pName, int IsDir, int DirType, void *pUser)
111{
112 CMenuBackground *pSelf = (CMenuBackground *)pUser;
113 const char *pSuffix = str_endswith(str: pName, suffix: ".map");
114 if(IsDir || !pSuffix)
115 return 0;
116 char aFullName[128];
117 char aThemeName[128];
118 str_truncate(dst: aFullName, dst_size: sizeof(aFullName), src: pName, truncation_len: pSuffix - pName);
119
120 bool IsDay = false;
121 bool IsNight = false;
122 if((pSuffix = str_endswith(str: aFullName, suffix: "_day")))
123 {
124 str_truncate(dst: aThemeName, dst_size: sizeof(aThemeName), src: pName, truncation_len: pSuffix - aFullName);
125 IsDay = true;
126 }
127 else if((pSuffix = str_endswith(str: aFullName, suffix: "_night")))
128 {
129 str_truncate(dst: aThemeName, dst_size: sizeof(aThemeName), src: pName, truncation_len: pSuffix - aFullName);
130 IsNight = true;
131 }
132 else
133 str_copy(dst&: aThemeName, src: aFullName);
134
135 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
136 return 0;
137
138 // try to edit an existing theme
139 for(auto &Theme : pSelf->m_vThemes)
140 {
141 if(str_comp(a: Theme.m_Name.c_str(), b: aThemeName) == 0)
142 {
143 if(IsDay)
144 Theme.m_HasDay = true;
145 if(IsNight)
146 Theme.m_HasNight = true;
147 return 0;
148 }
149 }
150
151 // make new theme
152 char aBuf[512];
153 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "added theme '%s' from 'themes/%s'", aThemeName, pName);
154 pSelf->Console()->Print(Level: IConsole::OUTPUT_LEVEL_ADDINFO, pFrom: "menuthemes", pStr: aBuf);
155 pSelf->m_vThemes.emplace_back(args&: aThemeName, args&: IsDay, args&: IsNight);
156 pSelf->LoadThemeIcon(Theme&: pSelf->m_vThemes.back());
157
158 if(time_get_nanoseconds() - pSelf->m_ThemeScanStartTime > 500ms)
159 {
160 pSelf->GameClient()->m_Menus.RenderLoading(pCaption: Localize(pStr: "Loading menu themes"), pContent: "", IncreaseCounter: 0);
161 }
162 return 0;
163}
164
165void CMenuBackground::LoadMenuBackground(bool HasDayHint, bool HasNightHint)
166{
167 if(!m_IsInit)
168 return;
169
170 if(m_Loaded && m_pMap == m_pBackgroundMap.get())
171 m_pMap->Unload();
172
173 m_Loaded = false;
174 m_pMap = m_pBackgroundMap.get();
175 m_pLayers = m_pBackgroundLayers;
176 m_pImages = m_pBackgroundImages;
177
178 ResetPositions();
179
180 str_copy(dst&: m_aMapName, src: g_Config.m_ClMenuMap);
181
182 if(g_Config.m_ClMenuMap[0] != '\0')
183 {
184 m_Loading = true;
185
186 const char *pMenuMap = g_Config.m_ClMenuMap;
187 if(str_comp(a: pMenuMap, b: "auto") == 0)
188 {
189 const ETimeSeason Season = time_season();
190 switch(Season)
191 {
192 case ETimeSeason::SPRING:
193 case ETimeSeason::EASTER:
194 pMenuMap = "heavens";
195 break;
196 case ETimeSeason::SUMMER:
197 pMenuMap = "jungle";
198 break;
199 case ETimeSeason::AUTUMN:
200 case ETimeSeason::HALLOWEEN:
201 pMenuMap = "autumn";
202 break;
203 case ETimeSeason::WINTER:
204 case ETimeSeason::XMAS:
205 pMenuMap = "winter";
206 break;
207 case ETimeSeason::NEWYEAR:
208 pMenuMap = "newyear";
209 break;
210 default:
211 dbg_assert_failed("Invalid season: %d", (int)Season);
212 }
213 }
214 else if(str_comp(a: pMenuMap, b: "rand") == 0)
215 {
216 // make sure to load themes
217 const std::vector<CTheme> &vThemesRef = GetThemes();
218 if(vThemesRef.size() > PREDEFINED_THEMES_COUNT)
219 {
220 int RandomThemeIndex = rand() % (vThemesRef.size() - PREDEFINED_THEMES_COUNT);
221 if(RandomThemeIndex + PREDEFINED_THEMES_COUNT < (int)vThemesRef.size())
222 pMenuMap = vThemesRef[RandomThemeIndex + PREDEFINED_THEMES_COUNT].m_Name.c_str();
223 }
224 }
225
226 char aBuf[128];
227
228 const int HourOfTheDay = time_houroftheday();
229 const bool IsDaytime = HourOfTheDay >= 6 && HourOfTheDay < 18;
230
231 if(!m_Loaded && ((HasDayHint && IsDaytime) || (HasNightHint && !IsDaytime)))
232 {
233 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "themes/%s_%s.map", pMenuMap, IsDaytime ? "day" : "night");
234 if(m_pMap->Load(pFullName: pMenuMap, pStorage: Storage(), pPath: aBuf, StorageType: IStorage::TYPE_ALL))
235 {
236 m_Loaded = true;
237 }
238 }
239
240 if(!m_Loaded)
241 {
242 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "themes/%s.map", pMenuMap);
243 if(m_pMap->Load(pFullName: pMenuMap, pStorage: Storage(), pPath: aBuf, StorageType: IStorage::TYPE_ALL))
244 {
245 m_Loaded = true;
246 }
247 }
248
249 if(!m_Loaded && ((HasDayHint && !IsDaytime) || (HasNightHint && IsDaytime)))
250 {
251 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "themes/%s_%s.map", pMenuMap, IsDaytime ? "night" : "day");
252 if(m_pMap->Load(pFullName: pMenuMap, pStorage: Storage(), pPath: aBuf, StorageType: IStorage::TYPE_ALL))
253 {
254 m_Loaded = true;
255 }
256 }
257
258 if(m_Loaded)
259 {
260 m_pLayers->Init(pMap: m_pMap, GameOnly: true);
261
262 m_pImages->LoadBackground(pLayers: m_pLayers, pMap: m_pMap);
263 CMapLayers::OnMapLoad();
264
265 // look for custom positions
266 CMapItemLayerTilemap *pTLayer = m_pLayers->GameLayer();
267 if(pTLayer)
268 {
269 int DataIndex = pTLayer->m_Data;
270 unsigned int Size = m_pLayers->Map()->GetDataSize(Index: DataIndex);
271 void *pTiles = m_pLayers->Map()->GetData(Index: DataIndex);
272 unsigned int TileSize = sizeof(CTile);
273
274 if(Size >= pTLayer->m_Width * pTLayer->m_Height * TileSize)
275 {
276 for(int y = 0; y < pTLayer->m_Height; ++y)
277 {
278 for(int x = 0; x < pTLayer->m_Width; ++x)
279 {
280 unsigned char Index = ((CTile *)pTiles)[y * pTLayer->m_Width + x].m_Index;
281 if(Index >= TILE_TIME_CHECKPOINT_FIRST && Index <= TILE_TIME_CHECKPOINT_LAST)
282 {
283 int ArrayIndex = std::clamp<int>(val: (Index - TILE_TIME_CHECKPOINT_FIRST), lo: 0, hi: NUM_POS);
284 m_aPositions[ArrayIndex] = vec2(x * 32.0f + 16.0f, y * 32.0f + 16.0f);
285 }
286
287 x += ((CTile *)pTiles)[y * pTLayer->m_Width + x].m_Skip;
288 }
289 }
290 }
291 }
292 }
293 m_Loading = false;
294 }
295}
296
297void CMenuBackground::OnMapLoad()
298{
299}
300
301void CMenuBackground::OnRender()
302{
303}
304
305bool CMenuBackground::Render()
306{
307 if(!m_Loaded)
308 return false;
309
310 m_Camera.m_Zoom = 0.7f;
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 * std::clamp(val: Client()->RenderFrameTime(), lo: 0.0f, hi: 0.1f);
317 m_CurrentDirection = rotate(a: m_CurrentDirection, angle: RotPerTick);
318 m_Camera.m_Center = m_RotationCenter + m_CurrentDirection * (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 m_CurrentDirection = normalize(v: m_AnimationStartPos - TargetPos);
332 else
333 m_CurrentDirection = vec2(1.0f, 0.0f);
334
335 // move time
336 m_MoveTime += std::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 + m_CurrentDirection * (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