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