]> de.git.xonotic.org Git - xonotic/xonotic-data.pk3dir.git/blob - qcsrc/menu/menu.qc
Fix compilation unit tester
[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                 if (vdist(pos - menuTooltipAveragedMousePos, <, 0.01))
528                 {
529                         it = m_findtooltipitem(main, pos);
530
531                         if (it.instanceOfListBox && it.isScrolling(it)) it = NULL;
532
533                         if (it && prev_tooltip != it.tooltip)
534                         {
535                                 // fade out if tooltip of a certain item has changed
536                                 menuTooltipState = 3;
537                                 if (prev_tooltip) strunzone(prev_tooltip);
538                                 prev_tooltip = strzone(it.tooltip);
539                         }
540                         else if (menuTooltipItem && !m_testmousetooltipbox(pos))
541                         {
542                                 menuTooltipState = 3;  // fade out if mouse touches it
543                         }
544                 }
545                 else
546                 {
547                         it = NULL;
548                 }
549         }
550         vector fontsize = '1 0 0' * (SKINFONTSIZE_TOOLTIP / conwidth) + '0 1 0' * (SKINFONTSIZE_TOOLTIP / conheight);
551
552         // float menuTooltipState; // 0: static, 1: fading in, 2: fading out, 3: forced fading out
553         if (it != menuTooltipItem)
554         {
555                 switch (menuTooltipState)
556                 {
557                         case 0:
558                                 if (menuTooltipItem)
559                                 {
560                                         // another item: fade out first
561                                         menuTooltipState = 2;
562                                 }
563                                 else
564                                 {
565                                         // new item: fade in
566                                         menuTooltipState = 1;
567                                         menuTooltipItem = it;
568
569                                         menuTooltipOrigin.x = -1;  // unallocated
570
571                                         if (menuTooltipText) strunzone(menuTooltipText);
572                                         menuTooltipText = strzone(gettooltip());
573
574                                         int i = 0;
575                                         float w = 0;
576                                         for (getWrappedLine_remaining = menuTooltipText; getWrappedLine_remaining; ++i)
577                                         {
578                                                 string s = getWrappedLine(SKINWIDTH_TOOLTIP, fontsize, draw_TextWidth_WithoutColors);
579                                                 float f = draw_TextWidth(s, false, fontsize);
580                                                 if (f > w) w = f;
581                                         }
582                                         menuTooltipSize.x = w + 2 * (SKINMARGIN_TOOLTIP_x / conwidth);
583                                         menuTooltipSize.y = i * fontsize.y + 2 * (SKINMARGIN_TOOLTIP_y / conheight);
584                                         menuTooltipSize.z = 0;
585                                 }
586                                 break;
587                         case 1:
588                                 // changing item while fading in: fade out first
589                                 menuTooltipState = 2;
590                                 break;
591                         case 2:
592                                 // changing item while fading out: can't
593                                 break;
594                 }
595         }
596         else if (menuTooltipState == 2)  // re-fade in?
597         {
598                 menuTooltipState = 1;
599         }
600
601         switch (menuTooltipState)
602         {
603                 case 1:  // fade in
604                         menuTooltipAlpha = bound(0, menuTooltipAlpha + 5 * frametime, 1);
605                         if (menuTooltipAlpha == 1) menuTooltipState = 0;
606                         break;
607                 case 2:  // fade out
608                 case 3:  // forced fade out
609                         menuTooltipAlpha = bound(0, menuTooltipAlpha - 2 * frametime, 1);
610                         if (menuTooltipAlpha == 0)
611                         {
612                                 menuTooltipState = 0;
613                                 menuTooltipItem = NULL;
614                         }
615                         break;
616         }
617
618         if (menuTooltipItem == NULL)
619         {
620                 if (menuTooltipText)
621                 {
622                         strunzone(menuTooltipText);
623                         menuTooltipText = string_null;
624                 }
625                 return;
626         }
627         else
628         {
629                 if (menu_tooltips != menu_tooltips_old)
630                 {
631                         if (menu_tooltips != 0 && menu_tooltips_old != 0) menuTooltipItem = NULL; // reload tooltip next frame
632                         menu_tooltips_old = menu_tooltips;
633                 }
634                 else if (menuTooltipOrigin.x < 0)                                             // unallocated?
635                 {
636                         m_allocatetooltipbox(pos);
637                 }
638                 if (menuTooltipOrigin.x >= 0)
639                 {
640                         // draw the tooltip!
641                         vector p = SKINBORDER_TOOLTIP;
642                         p.x *= 1 / conwidth;
643                         p.y *= 1 / conheight;
644                         draw_BorderPicture(menuTooltipOrigin, SKINGFX_TOOLTIP, menuTooltipSize, '1 1 1', menuTooltipAlpha, p);
645                         p = menuTooltipOrigin;
646                         p.x += SKINMARGIN_TOOLTIP_x / conwidth;
647                         p.y += SKINMARGIN_TOOLTIP_y / conheight;
648                         for (getWrappedLine_remaining = menuTooltipText; getWrappedLine_remaining; p.y += fontsize.y)
649                         {
650                                 string s = getWrappedLine(SKINWIDTH_TOOLTIP, fontsize, draw_TextWidth_WithoutColors);
651                                 draw_Text(p, s, fontsize, SKINCOLOR_TOOLTIP, SKINALPHA_TOOLTIP * menuTooltipAlpha, false);
652                         }
653                 }
654         }
655 }
656
657 void m_draw(float width, float height)
658 {
659         m_gamestatus();
660
661         execute_next_frame();
662
663         menuMouseMode = cvar("menu_mouse_absolute");
664
665         if (anim) anim.tickAll(anim);
666
667         UpdateConWidthHeight(width, height, cvar("vid_pixelheight"));
668
669         if (!menuInitialized)
670         {
671                 // TODO draw an info image about this situation
672                 m_init_delayed();
673                 return;
674         }
675         if (!menuNotTheFirstFrame)
676         {
677                 menuNotTheFirstFrame = true;
678                 if (Menu_Active && !cvar("menu_video_played"))
679         {
680             localcmd("cd loop $menu_cdtrack; play sound/announcer/default/welcome.wav\n");
681             menuLogoAlpha = -0.8;  // no idea why, but when I start this at zero, it jumps instead of fading FIXME
682         }
683                 // ALWAYS set this cvar; if we start but menu is not active, this means we want no background music!
684                 localcmd("set menu_video_played 1\n");
685         }
686
687         float t = gettime();
688         float realFrametime = frametime = min(0.2, t - menuPrevTime);
689         menuPrevTime = t;
690         time += frametime;
691
692         t = cvar("menu_slowmo");
693         if (t)
694         {
695                 frametime *= t;
696                 realFrametime *= t;
697         }
698         else
699         {
700                 t = 1;
701         }
702
703         if (Menu_Active)
704         {
705                 if (getmousetarget() == (menuMouseMode ? MT_CLIENT : MT_MENU)
706                     && (getkeydest() == KEY_MENU || getkeydest() == KEY_MENU_GRABBED))
707                         setkeydest(keyGrabber ? KEY_MENU_GRABBED : KEY_MENU);
708                 else m_hide();
709         }
710
711         if (cvar("cl_capturevideo")) frametime = t / cvar("cl_capturevideo_fps");  // make capturevideo work smoothly
712
713         prevMenuAlpha = menuAlpha;
714         if (Menu_Active)
715         {
716                 if (menuAlpha == 0 && menuLogoAlpha < 2)
717                 {
718                         menuLogoAlpha += 2 * frametime;
719                 }
720                 else
721                 {
722                         menuAlpha = min(1, menuAlpha + 5 * frametime);
723                         menuLogoAlpha = 2;
724                 }
725         }
726         else
727         {
728                 menuAlpha = max(0, menuAlpha - 5 * frametime);
729                 menuLogoAlpha = 2;
730         }
731
732         draw_reset_cropped();
733
734         if (!(gamestatus & (GAME_CONNECTED | GAME_ISSERVER)))
735         {
736                 if (menuLogoAlpha > 0)
737                 {
738                         draw_reset_full();
739                         draw_Fill('0 0 0', '1 1 0', SKINCOLOR_BACKGROUND, 1);
740                         drawBackground(SKINGFX_BACKGROUND, bound(0, menuLogoAlpha, 1), SKINALIGN_BACKGROUND, true);
741                         draw_reset_cropped();
742                         if (menuAlpha <= 0 && SKINALPHA_CURSOR_INTRO > 0)
743                         {
744                                 draw_alpha = SKINALPHA_CURSOR_INTRO * bound(0, menuLogoAlpha, 1);
745                                 draw_drawMousePointer(menuMousePos);
746                                 draw_alpha = 1;
747                         }
748                 }
749         }
750         else if (SKINALPHA_BACKGROUND_INGAME)
751         {
752                 if (menuAlpha > 0)
753                 {
754                         draw_reset_full();
755                         drawBackground(SKINGFX_BACKGROUND_INGAME, menuAlpha * SKINALPHA_BACKGROUND_INGAME,
756                                 SKINALIGN_BACKGROUND_INGAME, false);
757                         draw_reset_cropped();
758                 }
759         }
760
761         if (menuAlpha != prevMenuAlpha) cvar_set("_menu_alpha", ftos(menuAlpha));
762
763         draw_reset_cropped();
764         preMenuDraw();
765         draw_reset_cropped();
766
767         if (menuAlpha <= 0)
768         {
769                 if (prevMenuAlpha > 0) main.initializeDialog(main, main.firstChild);
770                 draw_reset_cropped();
771                 postMenuDraw();
772                 return;
773         }
774
775         draw_alpha *= menuAlpha;
776
777         if (!Menu_Active)
778         {
779                 // do not update mouse position
780                 // it prevents mouse jumping to '0 0 0' when menu is fading out
781         }
782         else if (menuMouseMode)
783         {
784                 vector newMouse = globalToBox(getmousepos(), draw_shift, draw_scale);
785                 if (newMouse != '0 0 0' && newMouse != menuMousePos)
786                 {
787                         menuMousePos = newMouse;
788                         if (mouseButtonsPressed) main.mouseDrag(main, menuMousePos);
789                         else main.mouseMove(main, menuMousePos);
790                 }
791         }
792         else if (frametime > 0)
793         {
794                 vector dMouse = getmousepos() * (frametime / realFrametime);  // for capturevideo
795                 if (dMouse != '0 0 0')
796                 {
797                         vector minpos = globalToBox('0 0 0', draw_shift, draw_scale);
798                         vector maxpos = globalToBox(eX * (realconwidth - 1) + eY * (realconheight - 1), draw_shift, draw_scale);
799                         dMouse = globalToBoxSize(dMouse, draw_scale);
800                         menuMousePos += dMouse * cvar("menu_mouse_speed");
801                         menuMousePos.x = bound(minpos.x, menuMousePos.x, maxpos.x);
802                         menuMousePos.y = bound(minpos.y, menuMousePos.y, maxpos.y);
803                         if (mouseButtonsPressed) main.mouseDrag(main, menuMousePos);
804                         else main.mouseMove(main, menuMousePos);
805                 }
806         }
807         main.draw(main);
808
809         m_tooltip(menuMousePos);
810
811         draw_alpha = max(draw_alpha, SKINALPHA_CURSOR_INTRO * bound(0, menuLogoAlpha, 1));
812
813         draw_drawMousePointer(menuMousePos);
814
815         draw_reset_cropped();
816         postMenuDraw();
817
818         frametime = 0;
819 }
820
821 void m_display()
822 {
823         Menu_Active = true;
824         setkeydest(KEY_MENU);
825         setmousetarget((menuMouseMode ? MT_CLIENT : MT_MENU));
826
827         if (!menuInitialized) return;
828
829         if (mouseButtonsPressed) main.mouseRelease(main, menuMousePos);
830         mouseButtonsPressed = 0;
831
832         main.focusEnter(main);
833         main.showNotify(main);
834 }
835
836 void m_hide()
837 {
838         Menu_Active = false;
839         setkeydest(KEY_GAME);
840         setmousetarget(MT_CLIENT);
841
842         if (!menuInitialized) return;
843
844         main.focusLeave(main);
845         main.hideNotify(main);
846 }
847
848 void m_toggle(int mode)
849 {
850         if (Menu_Active)
851         {
852                 if (mode == 1) return;
853                 m_hide();
854         }
855         else
856         {
857                 if (mode == 0) return;
858                 m_display();
859         }
860 }
861
862 void Shutdown()
863 {
864         m_hide();
865         FOREACH_ENTITY_ORDERED(it.destroy, LAMBDA(
866                 if (it.classname == "vtbl") continue;
867                 it.destroy(it);
868         ));
869 }
870
871 void m_focus_item_chain(entity outermost, entity innermost)
872 {
873         if (innermost.parent != outermost) m_focus_item_chain(outermost, innermost.parent);
874         innermost.parent.setFocus(innermost.parent, innermost);
875 }
876
877 void m_activate_window(entity wnd)
878 {
879         entity par = wnd.parent;
880         if (par) m_activate_window(par);
881
882         if (par.instanceOfModalController)
883         {
884                 if (wnd.tabSelectingButton)
885                         // tabs
886                         TabButton_Click(wnd.tabSelectingButton, wnd);
887                 else
888                         // root
889                         par.initializeDialog(par, wnd);
890         }
891         else if (par.instanceOfNexposee)
892         {
893                 // nexposee (sorry for violating abstraction here)
894                 par.selectedChild = wnd;
895                 par.animationState = 1;
896                 Container_setFocus(par, NULL);
897         }
898         else if (par.instanceOfContainer)
899         {
900                 // other containers
901                 if (par.focused) par.setFocus(par, wnd);
902         }
903 }
904
905 void m_setpointerfocus(entity wnd)
906 {
907         if (!wnd.instanceOfContainer) return;
908         entity focus = wnd.preferredFocusedGrandChild(wnd);
909         if (!focus) return;
910         menuMousePos = focus.origin + 0.5 * focus.size;
911         menuMousePos.x *= 1 / conwidth;
912         menuMousePos.y *= 1 / conheight;
913         entity par = wnd.parent;
914         if (par.focused) par.setFocus(par, wnd);
915         if (wnd.focused) m_focus_item_chain(wnd, focus);
916 }
917
918 void m_goto(string itemname)
919 {
920         if (!menuInitialized)
921         {
922                 if (m_goto_buffer) strunzone(m_goto_buffer);
923                 m_goto_buffer = strzone(itemname);
924                 return;
925         }
926         if (itemname == "")  // this can be called by GameCommand
927         {
928                 if (gamestatus & (GAME_ISSERVER | GAME_CONNECTED))
929                 {
930                         m_hide();
931                 }
932                 else
933                 {
934                         m_activate_window(main.mainNexposee);
935                         m_display();
936                 }
937         }
938         else
939         {
940                 entity e;
941                 for (e = NULL; (e = find(e, name, itemname)); )
942                         if (e.classname != "vtbl") break;
943
944                 if ((e) && (!e.requiresConnection || (gamestatus & (GAME_ISSERVER | GAME_CONNECTED))))
945                 {
946                         m_hide();
947                         m_activate_window(e);
948                         m_setpointerfocus(e);
949                         m_display();
950                 }
951         }
952 }
953
954 void m_play_focus_sound()
955 {
956         static float menuLastFocusSoundTime;
957         if (cvar("menu_sounds") < 2) return;
958         if (time - menuLastFocusSoundTime <= 0.25) return;
959         localsound(MENU_SOUND_FOCUS);
960         menuLastFocusSoundTime = time;
961 }
962
963 void m_play_click_sound(string soundfile)
964 {
965         if (!cvar("menu_sounds")) return;
966         localsound(soundfile);
967 }