]> de.git.xonotic.org Git - xonotic/xonotic-data.pk3dir.git/blob - qcsrc/menu/menu.qc
Merge branch 'master' into terencehill/menu_optimization
[xonotic/xonotic-data.pk3dir.git] / qcsrc / menu / menu.qc
1 #include "menu.qh"
2 #include "classes.qc"
3 #include "xonotic/util.qh"
4
5 #include "../common/items/all.qh"
6 #include "../common/weapons/all.qh"
7 #include "../common/mapinfo.qh"
8 #include "../common/mutators/base.qh"
9
10 int mouseButtonsPressed;
11 vector menuMousePos;
12 int menuShiftState;
13 float menuPrevTime;
14 float menuAlpha;
15 float menuLogoAlpha;
16 float prevMenuAlpha;
17 bool menuInitialized;
18 bool menuNotTheFirstFrame;
19 int menuMouseMode;
20
21 float conwidth_s, conheight_s;
22 float vidwidth_s, vidheight_s, vidpixelheight_s;
23 float realconwidth, realconheight;
24
25 void m_sync()
26 {
27         updateCompression();
28         vidwidth_s = vidheight_s = vidpixelheight_s = 0;  // Force updateConwidths on next draw
29
30         loadAllCvars(main);
31 }
32
33 void m_gamestatus()
34 {
35         gamestatus = 0;
36         if (isserver()) gamestatus |= GAME_ISSERVER;
37         if (clientstate() == CS_CONNECTED || isdemo()) gamestatus |= GAME_CONNECTED;
38         if (cvar("developer")) gamestatus |= GAME_DEVELOPER;
39 }
40
41 void m_init()
42 {
43         bool restarting = false;
44         cvar_set("_menu_alpha", "0");
45         prvm_language = cvar_string("prvm_language");
46         if (prvm_language == "")
47         {
48                 prvm_language = "en";
49                 cvar_set("prvm_language", prvm_language);
50                 localcmd("\nmenu_restart\n");
51                 restarting = true;
52         }
53         prvm_language = strzone(prvm_language);
54         cvar_set("_menu_prvm_language", prvm_language);
55
56 #ifdef WATERMARK
57                 LOG_INFOF("^4MQC Build information: ^1%s\n", WATERMARK);
58 #endif
59
60         // list all game dirs (TEST)
61         if (cvar("developer"))
62         {
63                 for (int i = 0; ; ++i)
64                 {
65                         string s = getgamedirinfo(i, GETGAMEDIRINFO_NAME);
66                         if (!s) break;
67                         LOG_TRACE(s, ": ", getgamedirinfo(i, GETGAMEDIRINFO_DESCRIPTION));
68                 }
69         }
70
71         // needs to be done so early because of the constants they create
72         static_init();
73         static_init_late();
74         static_init_precache();
75
76         RegisterSLCategories();
77
78         float ddsload = cvar("r_texture_dds_load");
79         float texcomp = cvar("gl_texturecompression");
80         updateCompression();
81         if (ddsload != cvar("r_texture_dds_load") || texcomp != cvar("gl_texturecompression")) localcmd("\nr_restart\n");
82
83         if (!restarting)
84         {
85                 if (cvar("_menu_initialized"))  // always show menu after menu_restart
86                         m_display();
87                 else m_hide();
88                 cvar_set("_menu_initialized", "1");
89         }
90 }
91
92 const float MENU_ASPECT = 1280 / 1024;
93
94 void draw_reset_cropped()
95 {
96         draw_reset(conwidth, conheight, 0.5 * (realconwidth - conwidth), 0.5 * (realconheight - conheight));
97 }
98 void draw_reset_full()
99 {
100         draw_reset(realconwidth, realconheight, 0, 0);
101 }
102
103 void UpdateConWidthHeight(float w, float h, float p)
104 {
105         if (w != vidwidth_s || h != vidheight_s || p != vidpixelheight_s)
106         {
107                 if (updateConwidths(w, h, p)) localcmd(sprintf("\nexec %s\n", cvar_string("menu_font_cfg")));
108                 vidwidth_s = w;
109                 vidheight_s = h;
110                 vidpixelheight_s = p;
111         }
112         conwidth_s = conwidth;
113         conheight_s = conheight;
114         realconwidth = cvar("vid_conwidth");
115         realconheight = cvar("vid_conheight");
116         if (realconwidth / realconheight > MENU_ASPECT)
117         {
118                 // widescreen
119                 conwidth = realconheight * MENU_ASPECT;
120                 conheight = realconheight;
121         }
122         else
123         {
124                 // squarescreen
125                 conwidth = realconwidth;
126                 conheight = realconwidth / MENU_ASPECT;
127         }
128         if (main)
129         {
130                 if (conwidth_s != conwidth || conheight_s != conheight)
131                 {
132                         draw_reset_cropped();
133                         main.resizeNotify(main, '0 0 0', eX * conwidth + eY * conheight, '0 0 0', eX * conwidth + eY * conheight);
134                 }
135         }
136         else
137         {
138                 vidwidth_s = vidheight_s = vidpixelheight_s = 0;  // retry next frame
139         }
140 }
141
142 string m_goto_buffer;
143 void m_init_delayed()
144 {
145         draw_reset_cropped();
146
147         menuInitialized = false;
148         if (!preMenuInit()) return;
149         menuInitialized = true;
150
151         int fh = -1;
152         if (cvar_string("menu_skin") != "")
153         {
154                 draw_currentSkin = strcat("gfx/menu/", cvar_string("menu_skin"));
155                 fh = fopen(strcat(draw_currentSkin, "/skinvalues.txt"), FILE_READ);
156         }
157         if (fh < 0 && cvar_defstring("menu_skin") != "")
158         {
159                 cvar_set("menu_skin", cvar_defstring("menu_skin"));
160                 draw_currentSkin = strcat("gfx/menu/", cvar_string("menu_skin"));
161                 fh = fopen(strcat(draw_currentSkin, "/skinvalues.txt"), FILE_READ);
162         }
163         if (fh < 0)
164         {
165                 draw_currentSkin = "gfx/menu/default";
166                 fh = fopen(strcat(draw_currentSkin, "/skinvalues.txt"), FILE_READ);
167         }
168         if (fh < 0) error("cannot load any menu skin\n");
169         draw_currentSkin = strzone(draw_currentSkin);
170         for (string s; (s = fgets(fh)); )
171         {
172                 // these two are handled by skinlist.qc
173                 if (substring(s, 0, 6) == "title ") continue;
174                 if (substring(s, 0, 7) == "author ") continue;
175                 int n = tokenize_console(s);
176                 if (n < 2) continue;
177                 Skin_ApplySetting(argv(0), substring(s, argv_start_index(1), argv_end_index(-1) - argv_start_index(1)));
178         }
179         fclose(fh);
180
181         int glob = search_begin(strcat(draw_currentSkin, "/*.tga"), true, true);
182         if (glob >= 0)
183         {
184                 for (int i = 0, n = search_getsize(glob); i < n; ++i)
185                         precache_pic(search_getfilename(glob, i));
186                 search_end(glob);
187         }
188
189         draw_setMousePointer(SKINGFX_CURSOR, SKINSIZE_CURSOR, SKINOFFSET_CURSOR);
190
191         anim = NEW(AnimHost);
192         main = NEW(MainWindow);
193         main.configureMainWindow(main);
194
195         main.resizeNotify(main, '0 0 0', eX * conwidth + eY * conheight, '0 0 0', eX * conwidth + eY * conheight);
196         main.focused = true;
197         menuShiftState = 0;
198         menuMousePos = '0.5 0.5 0';
199
200         m_sync();
201
202         if (m_goto_buffer)
203         {
204                 m_goto(m_goto_buffer);
205                 strunzone(m_goto_buffer);
206                 m_goto_buffer = string_null;
207         }
208
209         if (Menu_Active) m_display();  // delayed menu display
210 }
211
212 void m_keyup(float key, float ascii)
213 {
214         if (!menuInitialized) return;
215         if (!Menu_Active) return;
216         draw_reset_cropped();
217         main.keyUp(main, key, ascii, menuShiftState);
218         if (key >= K_MOUSE1 && key <= K_MOUSE3)
219         {
220                 --mouseButtonsPressed;
221                 if (!mouseButtonsPressed) main.mouseRelease(main, menuMousePos);
222                 if (mouseButtonsPressed < 0)
223                 {
224                         mouseButtonsPressed = 0;
225                         LOG_TRACE("Warning: released an already released button\n");
226                 }
227         }
228         if (key == K_ALT) menuShiftState &= ~S_ALT;
229         if (key == K_CTRL) menuShiftState &= ~S_CTRL;
230         if (key == K_SHIFT) menuShiftState &= ~S_SHIFT;
231 }
232
233 void m_keydown(float key, float ascii)
234 {
235         if (!menuInitialized) return;
236         if (!Menu_Active) return;
237
238         if (menuMouseMode && key >= K_MOUSE1 && key <= K_MOUSE3)
239         {
240                 // detect a click outside of the game window
241                 vector p = getmousepos();
242                 if (p.x < 0 || p.x > realconwidth || p.y < 0 || p.y > realconheight)
243                 {
244                         ++mouseButtonsPressed;
245                         return;
246                 }
247         }
248
249         if (keyGrabber)
250         {
251                 entity e = keyGrabber;
252                 keyGrabber = NULL;
253                 e.keyGrabbed(e, key, ascii);
254         }
255         else
256         {
257                 draw_reset_cropped();
258                 if (!mouseButtonsPressed && key >= K_MOUSE1 && key <= K_MOUSE3) main.mousePress(main, menuMousePos);
259                 if (!main.keyDown(main, key, ascii, menuShiftState))
260                 {
261                         // disable menu on unhandled ESC
262                         if (key == K_ESCAPE)
263                                 if (gamestatus & (GAME_ISSERVER | GAME_CONNECTED))  // don't back out to console only
264                                         m_hide();
265                 }
266         }
267         if (key >= K_MOUSE1 && key <= K_MOUSE3)
268         {
269                 ++mouseButtonsPressed;
270                 if (mouseButtonsPressed > 10)
271                 {
272                         mouseButtonsPressed = 10;
273                         LOG_TRACE("Warning: pressed an already pressed button\n");
274                 }
275         }
276         if (key == K_ALT) menuShiftState |= S_ALT;
277         if (key == K_CTRL) menuShiftState |= S_CTRL;
278         if (key == K_SHIFT) menuShiftState |= S_SHIFT;
279 }
280
281 enum {
282         SCALEMODE_CROP,
283         SCALEMODE_LETTERBOX,
284         SCALEMODE_WIDTH,
285         SCALEMODE_HEIGHT,
286         SCALEMODE_STRETCH,
287 };
288 void draw_Picture_Aligned(vector algn, float scalemode, string img, float a)
289 {
290         vector sz = draw_PictureSize(img);
291         bool width_is_larger = (sz.x * draw_scale.y >= sz.y * draw_scale.x);
292         vector isz_w = '1 0 0' + '0 1 0' * ((sz.y / sz.x) * (draw_scale.x / draw_scale.y));
293         vector isz_h = '0 1 0' + '1 0 0' * ((sz.x / sz.y) * (draw_scale.y / draw_scale.x));
294         vector isz;
295         switch (scalemode)
296         {
297                 default:
298                 case SCALEMODE_CROP:
299                         isz = (width_is_larger ? isz_h : isz_w);
300                         break;
301                 case SCALEMODE_LETTERBOX:
302                         isz = (width_is_larger ? isz_w : isz_h);
303                         break;
304                 case SCALEMODE_WIDTH:
305                         isz = isz_w;
306                         break;
307                 case SCALEMODE_HEIGHT:
308                         isz = isz_h;
309                         break;
310                 case SCALEMODE_STRETCH:
311                         isz = '1 1 0';
312                         break;
313         }
314         vector org = eX * (algn.x * (1 - isz.x)) + eY * (algn.y * (1 - isz.y));
315         draw_Picture(org, img, isz, '1 1 1', a);
316 }
317
318 void drawBackground(string img, float a, string algn, float force1)
319 {
320         if (main.mainNexposee.ModalController_state == 0) return;
321         vector v = '0 0 0';
322         int scalemode = SCALEMODE_CROP;
323         for (int i = 0, l = 0; i < strlen(algn); ++i)
324         {
325                 string c = substring(algn, i, 1);
326                 switch (c)
327                 {
328                         case "c":
329                                 scalemode = SCALEMODE_CROP;
330                                 goto nopic;
331                         case "l":
332                                 scalemode = SCALEMODE_LETTERBOX;
333                                 goto nopic;
334                         case "h":
335                                 scalemode = SCALEMODE_HEIGHT;
336                                 goto nopic;
337                         case "w":
338                                 scalemode = SCALEMODE_WIDTH;
339                                 goto nopic;
340                         case "s":
341                                 scalemode = SCALEMODE_STRETCH;
342                                 goto nopic;
343                         case "1": case "4": case "7":
344                                 v.x = 0.0;
345                                 break;
346                         case "2": case "5": case "8":
347                                 v.x = 0.5;
348                                 break;
349                         case "3": case "6": case "9":
350                                 v.x = 1.0;
351                                 break;
352                         default:
353                                 v.x = random();
354                                 break;
355                 }
356                 switch (c)
357                 {
358                         case "7": case "8": case "9":
359                                 v.y = 0.0;
360                                 break;
361                         case "4": case "5": case "6":
362                                 v.y = 0.5;
363                                 break;
364                         case "1": case "2": case "3":
365                                 v.y = 1.0;
366                                 break;
367                         default:
368                                 v.y = random();
369                                 break;
370                 }
371                 if (l == 0)
372                 {
373                         draw_Picture_Aligned(v, scalemode, img, a);
374                 }
375                 else if (force1)
376                 {
377                         // force all secondary layers to use alpha 1. Prevents ugly issues
378                         // with overlap. It's a flag because it cannot be used for the
379                         // ingame background
380                         draw_Picture_Aligned(v, scalemode, strcat(img, "_l", ftos(l + 1)), 1);
381                 }
382                 else
383                 {
384                         draw_Picture_Aligned(v, scalemode, strcat(img, "_l", ftos(l + 1)), a);
385                 }
386                 ++l;
387                 : nopic
388         }
389 }
390
391 int menu_tooltips;
392 int menu_tooltips_old;
393 vector menuTooltipAveragedMousePos;
394 entity menuTooltipItem;
395 vector menuTooltipOrigin;
396 vector menuTooltipSize;
397 float menuTooltipAlpha;
398 string menuTooltipText;
399 int menuTooltipState;  // 0: static, 1: fading in, 2: fading out, 3: forced fading out
400 bool m_testmousetooltipbox(vector pos)
401 {
402         return !(
403             (pos.x >= menuTooltipOrigin.x && pos.x < menuTooltipOrigin.x + menuTooltipSize.x)
404             && (pos.y >= menuTooltipOrigin.y && pos.y < menuTooltipOrigin.y + menuTooltipSize.y)
405                 );
406 }
407 bool m_testtooltipbox(vector tooltippos)
408 {
409         if (tooltippos.x < 0) return false;
410         if (tooltippos.y < 0) return false;
411         if (tooltippos.x + menuTooltipSize.x > 1) return false;
412         if (tooltippos.y + menuTooltipSize.y > 1) return false;
413         menuTooltipOrigin = tooltippos;
414         return true;
415 }
416 bool m_allocatetooltipbox(vector pos)
417 {
418         vector avoidplus;
419         avoidplus.x = (SKINAVOID_TOOLTIP_x + SKINSIZE_CURSOR_x - SKINOFFSET_CURSOR_x * SKINSIZE_CURSOR_x) / conwidth;
420         avoidplus.y = (SKINAVOID_TOOLTIP_y + SKINSIZE_CURSOR_y - SKINOFFSET_CURSOR_y * SKINSIZE_CURSOR_y) / conheight;
421         avoidplus.z = 0;
422
423         vector avoidminus;
424         avoidminus.x = (SKINAVOID_TOOLTIP_x + SKINOFFSET_CURSOR_x * SKINSIZE_CURSOR_x) / conwidth + menuTooltipSize.x;
425         avoidminus.y = (SKINAVOID_TOOLTIP_y + SKINOFFSET_CURSOR_y * SKINSIZE_CURSOR_y) / conheight + menuTooltipSize.y;
426         avoidminus.z = 0;
427
428         // bottom right
429         vector v = pos + avoidplus;
430         if (m_testtooltipbox(v)) return true;
431
432         // bottom center
433         v.x = pos.x - menuTooltipSize.x * 0.5;
434         if (m_testtooltipbox(v)) return true;
435
436         // bottom left
437         v.x = pos.x - avoidminus.x;
438         if (m_testtooltipbox(v)) return true;
439
440         // top left
441         v.y = pos.y - avoidminus.y;
442         if (m_testtooltipbox(v)) return true;
443
444         // top center
445         v.x = pos.x - menuTooltipSize.x * 0.5;
446         if (m_testtooltipbox(v)) return true;
447
448         // top right
449         v.x = pos.x + avoidplus.x;
450         if (m_testtooltipbox(v)) return true;
451
452         return false;
453 }
454 entity m_findtooltipitem(entity root, vector pos)
455 {
456         entity best = NULL;
457         for (entity it = root; it.instanceOfContainer; )
458         {
459                 while (it.instanceOfNexposee && it.focusedChild)
460                 {
461                         it = it.focusedChild;
462                         pos = globalToBox(pos, it.Container_origin, it.Container_size);
463                 }
464                 if (it.instanceOfNexposee)
465                 {
466                         it = it.itemFromPoint(it, pos);
467                         if (it.tooltip) best = it;
468                         else if (menu_tooltips == 2 && (it.cvarName || it.onClickCommand)) best = it;
469                         it = NULL;
470                 }
471                 else if (it.instanceOfModalController)
472                 {
473                         it = it.focusedChild;
474                 }
475                 else
476                 {
477                         it = it.itemFromPoint(it, pos);
478                 }
479                 if (!it) break;
480                 if (it.tooltip) best = it;
481                 else if (menu_tooltips == 2 && (it.cvarName || it.onClickCommand)) best = it;
482                 pos = globalToBox(pos, it.Container_origin, it.Container_size);
483         }
484
485         return best;
486 }
487 string gettooltip()
488 {
489         if (menu_tooltips == 2)
490         {
491                 string s;
492                 if (menuTooltipItem.cvarName)
493                 {
494                         if (getCvarsMulti(menuTooltipItem)) s =
495                                     strcat("[", menuTooltipItem.cvarName, " ", getCvarsMulti(menuTooltipItem), "]");
496                         else s = strcat("[", menuTooltipItem.cvarName, "]");
497                 }
498                 else if (menuTooltipItem.onClickCommand)
499                 {
500                         s = strcat("<", menuTooltipItem.onClickCommand, ">");
501                 }
502                 else
503                 {
504                         return menuTooltipItem.tooltip;
505                 }
506                 if (menuTooltipItem.tooltip) return strcat(menuTooltipItem.tooltip, " ", s);
507                 return s;
508         }
509         return menuTooltipItem.tooltip;
510 }
511 void m_tooltip(vector pos)
512 {
513         static string prev_tooltip;
514         entity it;
515         menu_tooltips = cvar("menu_tooltips");
516         if (!menu_tooltips)
517         {
518                 // don't return immediately, fade out the active tooltip first
519                 if (menuTooltipItem == NULL) return;
520                 it = NULL;
521                 menu_tooltips_old = menu_tooltips;
522         }
523         else
524         {
525                 float f = bound(0, frametime * 2, 1);
526                 menuTooltipAveragedMousePos = menuTooltipAveragedMousePos * (1 - f) + pos * f;
527                 f = vlen(pos - menuTooltipAveragedMousePos);
528                 if (f < 0.01)
529                 {
530                         it = m_findtooltipitem(main, pos);
531
532                         if (it.instanceOfListBox && it.isScrolling(it)) it = world;
533
534                         if (it && prev_tooltip != it.tooltip)
535                         {
536                                 // fade out if tooltip of a certain item has changed
537                                 menuTooltipState = 3;
538                                 if (prev_tooltip) strunzone(prev_tooltip);
539                                 prev_tooltip = strzone(it.tooltip);
540                         }
541                         else if (menuTooltipItem && !m_testmousetooltipbox(pos))
542                         {
543                                 menuTooltipState = 3;  // fade out if mouse touches it
544                         }
545                 }
546                 else
547                 {
548                         it = NULL;
549                 }
550         }
551         vector fontsize = '1 0 0' * (SKINFONTSIZE_TOOLTIP / conwidth) + '0 1 0' * (SKINFONTSIZE_TOOLTIP / conheight);
552
553         // float menuTooltipState; // 0: static, 1: fading in, 2: fading out, 3: forced fading out
554         if (it != menuTooltipItem)
555         {
556                 switch (menuTooltipState)
557                 {
558                         case 0:
559                                 if (menuTooltipItem)
560                                 {
561                                         // another item: fade out first
562                                         menuTooltipState = 2;
563                                 }
564                                 else
565                                 {
566                                         // new item: fade in
567                                         menuTooltipState = 1;
568                                         menuTooltipItem = it;
569
570                                         menuTooltipOrigin.x = -1;  // unallocated
571
572                                         if (menuTooltipText) strunzone(menuTooltipText);
573                                         menuTooltipText = strzone(gettooltip());
574
575                                         int i = 0;
576                                         float w = 0;
577                                         for (getWrappedLine_remaining = menuTooltipText; getWrappedLine_remaining; ++i)
578                                         {
579                                                 string s = getWrappedLine(SKINWIDTH_TOOLTIP, fontsize, draw_TextWidth_WithoutColors);
580                                                 float f = draw_TextWidth(s, false, fontsize);
581                                                 if (f > w) w = f;
582                                         }
583                                         menuTooltipSize.x = w + 2 * (SKINMARGIN_TOOLTIP_x / conwidth);
584                                         menuTooltipSize.y = i * fontsize.y + 2 * (SKINMARGIN_TOOLTIP_y / conheight);
585                                         menuTooltipSize.z = 0;
586                                 }
587                                 break;
588                         case 1:
589                                 // changing item while fading in: fade out first
590                                 menuTooltipState = 2;
591                                 break;
592                         case 2:
593                                 // changing item while fading out: can't
594                                 break;
595                 }
596         }
597         else if (menuTooltipState == 2)  // re-fade in?
598         {
599                 menuTooltipState = 1;
600         }
601
602         switch (menuTooltipState)
603         {
604                 case 1:  // fade in
605                         menuTooltipAlpha = bound(0, menuTooltipAlpha + 5 * frametime, 1);
606                         if (menuTooltipAlpha == 1) menuTooltipState = 0;
607                         break;
608                 case 2:  // fade out
609                 case 3:  // forced fade out
610                         menuTooltipAlpha = bound(0, menuTooltipAlpha - 2 * frametime, 1);
611                         if (menuTooltipAlpha == 0)
612                         {
613                                 menuTooltipState = 0;
614                                 menuTooltipItem = NULL;
615                         }
616                         break;
617         }
618
619         if (menuTooltipItem == NULL)
620         {
621                 if (menuTooltipText)
622                 {
623                         strunzone(menuTooltipText);
624                         menuTooltipText = string_null;
625                 }
626                 return;
627         }
628         else
629         {
630                 if (menu_tooltips != menu_tooltips_old)
631                 {
632                         if (menu_tooltips != 0 && menu_tooltips_old != 0) menuTooltipItem = NULL; // reload tooltip next frame
633                         menu_tooltips_old = menu_tooltips;
634                 }
635                 else if (menuTooltipOrigin.x < 0)                                             // unallocated?
636                 {
637                         m_allocatetooltipbox(pos);
638                 }
639                 if (menuTooltipOrigin.x >= 0)
640                 {
641                         // draw the tooltip!
642                         vector p = SKINBORDER_TOOLTIP;
643                         p.x *= 1 / conwidth;
644                         p.y *= 1 / conheight;
645                         draw_BorderPicture(menuTooltipOrigin, SKINGFX_TOOLTIP, menuTooltipSize, '1 1 1', menuTooltipAlpha, p);
646                         p = menuTooltipOrigin;
647                         p.x += SKINMARGIN_TOOLTIP_x / conwidth;
648                         p.y += SKINMARGIN_TOOLTIP_y / conheight;
649                         for (getWrappedLine_remaining = menuTooltipText; getWrappedLine_remaining; p.y += fontsize.y)
650                         {
651                                 string s = getWrappedLine(SKINWIDTH_TOOLTIP, fontsize, draw_TextWidth_WithoutColors);
652                                 draw_Text(p, s, fontsize, SKINCOLOR_TOOLTIP, SKINALPHA_TOOLTIP * menuTooltipAlpha, false);
653                         }
654                 }
655         }
656 }
657
658 void m_draw(float width, float height)
659 {
660         m_gamestatus();
661
662         execute_next_frame();
663
664         menuMouseMode = cvar("menu_mouse_absolute");
665
666         if (anim) anim.tickAll(anim);
667
668         UpdateConWidthHeight(width, height, cvar("vid_pixelheight"));
669
670         if (!menuInitialized)
671         {
672                 // TODO draw an info image about this situation
673                 m_init_delayed();
674                 return;
675         }
676         if (!menuNotTheFirstFrame)
677         {
678                 menuNotTheFirstFrame = true;
679                 if (Menu_Active && !cvar("menu_video_played"))
680         {
681             localcmd("cd loop $menu_cdtrack; play sound/announcer/default/welcome.wav\n");
682             menuLogoAlpha = -0.8;  // no idea why, but when I start this at zero, it jumps instead of fading FIXME
683         }
684                 // ALWAYS set this cvar; if we start but menu is not active, this means we want no background music!
685                 localcmd("set menu_video_played 1\n");
686         }
687
688         float t = gettime();
689         float realFrametime = frametime = min(0.2, t - menuPrevTime);
690         menuPrevTime = t;
691         time += frametime;
692
693         t = cvar("menu_slowmo");
694         if (t)
695         {
696                 frametime *= t;
697                 realFrametime *= t;
698         }
699         else
700         {
701                 t = 1;
702         }
703
704         if (Menu_Active)
705         {
706                 if (getmousetarget() == (menuMouseMode ? MT_CLIENT : MT_MENU)
707                     && (getkeydest() == KEY_MENU || getkeydest() == KEY_MENU_GRABBED))
708                         setkeydest(keyGrabber ? KEY_MENU_GRABBED : KEY_MENU);
709                 else m_hide();
710         }
711
712         if (cvar("cl_capturevideo")) frametime = t / cvar("cl_capturevideo_fps");  // make capturevideo work smoothly
713
714         prevMenuAlpha = menuAlpha;
715         if (Menu_Active)
716         {
717                 if (menuAlpha == 0 && menuLogoAlpha < 2)
718                 {
719                         menuLogoAlpha += 2 * frametime;
720                 }
721                 else
722                 {
723                         menuAlpha = min(1, menuAlpha + 5 * frametime);
724                         menuLogoAlpha = 2;
725                 }
726         }
727         else
728         {
729                 menuAlpha = max(0, menuAlpha - 5 * frametime);
730                 menuLogoAlpha = 2;
731         }
732
733         draw_reset_cropped();
734
735         if (!(gamestatus & (GAME_CONNECTED | GAME_ISSERVER)))
736         {
737                 if (menuLogoAlpha > 0)
738                 {
739                         draw_reset_full();
740                         draw_Fill('0 0 0', '1 1 0', SKINCOLOR_BACKGROUND, 1);
741                         drawBackground(SKINGFX_BACKGROUND, bound(0, menuLogoAlpha, 1), SKINALIGN_BACKGROUND, true);
742                         draw_reset_cropped();
743                         if (menuAlpha <= 0 && SKINALPHA_CURSOR_INTRO > 0)
744                         {
745                                 draw_alpha = SKINALPHA_CURSOR_INTRO * bound(0, menuLogoAlpha, 1);
746                                 draw_drawMousePointer(menuMousePos);
747                                 draw_alpha = 1;
748                         }
749                 }
750         }
751         else if (SKINALPHA_BACKGROUND_INGAME)
752         {
753                 if (menuAlpha > 0)
754                 {
755                         draw_reset_full();
756                         drawBackground(SKINGFX_BACKGROUND_INGAME, menuAlpha * SKINALPHA_BACKGROUND_INGAME,
757                                 SKINALIGN_BACKGROUND_INGAME, false);
758                         draw_reset_cropped();
759                 }
760         }
761
762         if (menuAlpha != prevMenuAlpha) cvar_set("_menu_alpha", ftos(menuAlpha));
763
764         draw_reset_cropped();
765         preMenuDraw();
766         draw_reset_cropped();
767
768         if (menuAlpha <= 0)
769         {
770                 if (prevMenuAlpha > 0) main.initializeDialog(main, main.firstChild);
771                 draw_reset_cropped();
772                 postMenuDraw();
773                 return;
774         }
775
776         draw_alpha *= menuAlpha;
777
778         if (!Menu_Active)
779         {
780                 // do not update mouse position
781                 // it prevents mouse jumping to '0 0 0' when menu is fading out
782         }
783         else if (menuMouseMode)
784         {
785                 vector newMouse = globalToBox(getmousepos(), draw_shift, draw_scale);
786                 if (newMouse != '0 0 0' && newMouse != menuMousePos)
787                 {
788                         menuMousePos = newMouse;
789                         if (mouseButtonsPressed) main.mouseDrag(main, menuMousePos);
790                         else main.mouseMove(main, menuMousePos);
791                 }
792         }
793         else if (frametime > 0)
794         {
795                 vector dMouse = getmousepos() * (frametime / realFrametime);  // for capturevideo
796                 if (dMouse != '0 0 0')
797                 {
798                         vector minpos = globalToBox('0 0 0', draw_shift, draw_scale);
799                         vector maxpos = globalToBox(eX * (realconwidth - 1) + eY * (realconheight - 1), draw_shift, draw_scale);
800                         dMouse = globalToBoxSize(dMouse, draw_scale);
801                         menuMousePos += dMouse * cvar("menu_mouse_speed");
802                         menuMousePos.x = bound(minpos.x, menuMousePos.x, maxpos.x);
803                         menuMousePos.y = bound(minpos.y, menuMousePos.y, maxpos.y);
804                         if (mouseButtonsPressed) main.mouseDrag(main, menuMousePos);
805                         else main.mouseMove(main, menuMousePos);
806                 }
807         }
808         main.draw(main);
809
810         m_tooltip(menuMousePos);
811
812         draw_alpha = max(draw_alpha, SKINALPHA_CURSOR_INTRO * bound(0, menuLogoAlpha, 1));
813
814         draw_drawMousePointer(menuMousePos);
815
816         draw_reset_cropped();
817         postMenuDraw();
818
819         frametime = 0;
820 }
821
822 void m_display()
823 {
824         Menu_Active = true;
825         setkeydest(KEY_MENU);
826         setmousetarget((menuMouseMode ? MT_CLIENT : MT_MENU));
827
828         if (!menuInitialized) return;
829
830         if (mouseButtonsPressed) main.mouseRelease(main, menuMousePos);
831         mouseButtonsPressed = 0;
832
833         main.focusEnter(main);
834         main.showNotify(main);
835 }
836
837 void m_hide()
838 {
839         Menu_Active = false;
840         setkeydest(KEY_GAME);
841         setmousetarget(MT_CLIENT);
842
843         if (!menuInitialized) return;
844
845         main.focusLeave(main);
846         main.hideNotify(main);
847 }
848
849 void m_toggle(int mode)
850 {
851         if (Menu_Active)
852         {
853                 if (mode == 1) return;
854                 m_hide();
855         }
856         else
857         {
858                 if (mode == 0) return;
859                 m_display();
860         }
861 }
862
863 void Shutdown()
864 {
865         m_hide();
866         FOREACH_ENTITY_ORDERED(it.destroy, LAMBDA(
867                 if (it.classname == "vtbl") continue;
868                 it.destroy(it);
869         ));
870 }
871
872 void m_focus_item_chain(entity outermost, entity innermost)
873 {
874         if (innermost.parent != outermost) m_focus_item_chain(outermost, innermost.parent);
875         innermost.parent.setFocus(innermost.parent, innermost);
876 }
877
878 void m_activate_window(entity wnd)
879 {
880         entity par = wnd.parent;
881         if (par) m_activate_window(par);
882
883         if (par.instanceOfModalController)
884         {
885                 if (wnd.tabSelectingButton)
886                         // tabs
887                         TabButton_Click(wnd.tabSelectingButton, wnd);
888                 else
889                         // root
890                         par.initializeDialog(par, wnd);
891         }
892         else if (par.instanceOfNexposee)
893         {
894                 // nexposee (sorry for violating abstraction here)
895                 par.selectedChild = wnd;
896                 par.animationState = 1;
897                 Container_setFocus(par, NULL);
898         }
899         else if (par.instanceOfContainer)
900         {
901                 // other containers
902                 if (par.focused) par.setFocus(par, wnd);
903         }
904 }
905
906 void m_setpointerfocus(entity wnd)
907 {
908         if (!wnd.instanceOfContainer) return;
909         entity focus = wnd.preferredFocusedGrandChild(wnd);
910         if (!focus) return;
911         menuMousePos = focus.origin + 0.5 * focus.size;
912         menuMousePos.x *= 1 / conwidth;
913         menuMousePos.y *= 1 / conheight;
914         entity par = wnd.parent;
915         if (par.focused) par.setFocus(par, wnd);
916         if (wnd.focused) m_focus_item_chain(wnd, focus);
917 }
918
919 void m_goto(string itemname)
920 {
921         if (!menuInitialized)
922         {
923                 if (m_goto_buffer) strunzone(m_goto_buffer);
924                 m_goto_buffer = strzone(itemname);
925                 return;
926         }
927         if (itemname == "")  // this can be called by GameCommand
928         {
929                 if (gamestatus & (GAME_ISSERVER | GAME_CONNECTED))
930                 {
931                         m_hide();
932                 }
933                 else
934                 {
935                         m_activate_window(main.mainNexposee);
936                         m_display();
937                 }
938         }
939         else
940         {
941                 entity e;
942                 for (e = NULL; (e = find(e, name, itemname)); )
943                         if (e.classname != "vtbl") break;
944
945                 if ((e) && (!e.requiresConnection || (gamestatus & (GAME_ISSERVER | GAME_CONNECTED))))
946                 {
947                         m_hide();
948                         m_activate_window(e);
949                         m_setpointerfocus(e);
950                         m_display();
951                 }
952         }
953 }
954
955 void m_play_focus_sound()
956 {
957         static float menuLastFocusSoundTime;
958         if (cvar("menu_sounds") < 2) return;
959         if (time - menuLastFocusSoundTime <= 0.25) return;
960         localsound(MENU_SOUND_FOCUS);
961         menuLastFocusSoundTime = time;
962 }
963
964 void m_play_click_sound(string soundfile)
965 {
966         if (!cvar("menu_sounds")) return;
967         localsound(soundfile);
968 }