]> git.xonotic.org Git - xonotic/xonotic-data.pk3dir.git/blob - qcsrc/server/intermission.qc
Fix #2850 "xonotic crashes when pressing restart level after match end in campaign"
[xonotic/xonotic-data.pk3dir.git] / qcsrc / server / intermission.qc
1 #include "mapvoting.qh"
2
3 #include <common/mapinfo.qh>
4 #include <common/util.qh>
5 #include <server/bot/api.qh>
6 #include <server/bot/default/cvars.qh>
7 #include <server/campaign.qh>
8 #include <server/client.qh>
9 #include <server/mapvoting.qh>
10 #include <server/scores_rules.qh>
11 #include <server/world.qh>
12
13 string GetGametype()
14 {
15         return MapInfo_Type_ToString(MapInfo_LoadedGametype);
16 }
17
18 string GetMapname()
19 {
20         return mapname;
21 }
22
23 float Map_Count, Map_Current;
24 string Map_Current_Name;
25
26 // NOTE: this now expects the map list to be already tokenized and the count in Map_Count
27 int GetMaplistPosition()
28 {
29         string map = GetMapname();
30         int idx = autocvar_g_maplist_index;
31
32         if(idx >= 0)
33         {
34                 if(idx < Map_Count)
35                 {
36                         if(map == argv(idx))
37                         {
38                                 return idx;
39                         }
40                 }
41         }
42
43         for(int pos = 0; pos < Map_Count; ++pos)
44         {
45                 if(map == argv(pos))
46                         return pos;
47         }
48
49         // resume normal maplist rotation if current map is not in g_maplist
50         return idx;
51 }
52
53 bool MapHasRightSize(string map)
54 {
55         int minplayers = max(0, floor(autocvar_minplayers));
56         if (teamplay)
57                 minplayers = max(0, floor(autocvar_minplayers_per_team) * AVAILABLE_TEAMS);
58         if (autocvar_g_maplist_check_waypoints
59                 && (currentbots || autocvar_bot_number || player_count < minplayers))
60         {
61                 string checkwp_msg = strcat("checkwp ", map);
62                 if(!fexists(strcat("maps/", map, ".waypoints")))
63                 {
64                         LOG_TRACE(checkwp_msg, ": no waypoints");
65                         return false;
66                 }
67                 LOG_TRACE(checkwp_msg, ": has waypoints");
68         }
69
70         if(autocvar_g_maplist_ignore_sizes)
71                 return true;
72
73         // open map size restriction file
74         if(!MapReadSizes(map))
75                 return true; // map has no size restrictions
76
77         string checksize_msg = strcat("MapHasRightSize ", map);
78         int player_limit = ((autocvar_g_maplist_sizes_count_maxplayers) ? GetPlayerLimit() : 0);
79         int pcount = ((player_limit > 0) ? min(player_count, player_limit) : player_count); // bind it to the player limit so that forced spectators don't influence the limits
80
81         if(!autocvar_g_maplist_sizes_count_bots)
82                 pcount -= currentbots;
83         pcount -= rint(cvar("g_maplist_sizes_specparty") * pcount);
84
85         // ensure small maps can be selected when pcount is low
86         if(map_minplayers <= (_MapInfo_GetTeamPlayBool(MapInfo_CurrentGametype()) ? 4 : 2))
87                 map_minplayers = 0;
88
89         if(pcount < map_minplayers)
90         {
91                 LOG_TRACE(checksize_msg, ": not enough");
92                 return false;
93         }
94         if(map_maxplayers && pcount > map_maxplayers)
95         {
96                 LOG_TRACE(checksize_msg, ": too many");
97                 return false;
98         }
99         LOG_TRACE(checksize_msg, ": right size");
100         return true;
101 }
102
103 string Map_Filename(int position)
104 {
105         return strcat("maps/", argv(position), ".bsp");
106 }
107
108 void Map_MarkAsRecent(string m)
109 {
110         cvar_set("g_maplist_mostrecent", strwords(cons(m, autocvar_g_maplist_mostrecent), max(0, autocvar_g_maplist_mostrecent_count)));
111 }
112
113 bool Map_IsRecent(string m)
114 {
115         return strhasword(autocvar_g_maplist_mostrecent, m);
116 }
117
118 bool Map_Check(int position, float pass)
119 {
120         string filename;
121         string map_next;
122         map_next = argv(position);
123         if(pass <= 1)
124         {
125                 if(Map_IsRecent(map_next))
126                         return false;
127         }
128         filename = Map_Filename(position);
129         if(MapInfo_CheckMap(map_next))
130         {
131                 if(pass == 2)
132                         return true;
133                 if(MapHasRightSize(map_next))
134                         return true;
135                 return false;
136         }
137         else
138                 LOG_DEBUG( "Couldn't select '", filename, "'..." );
139
140         return false;
141 }
142
143 void Map_Goto_SetStr(string nextmapname)
144 {
145         if(getmapname_stored != "")
146                 strunzone(getmapname_stored);
147         if(nextmapname == "")
148                 getmapname_stored = "";
149         else
150                 getmapname_stored = strzone(nextmapname);
151 }
152
153 void Map_Goto_SetFloat(float position)
154 {
155         cvar_set("g_maplist_index", ftos(position));
156         Map_Goto_SetStr(argv(position));
157 }
158
159 void Map_Goto(float reinit)
160 {
161         MapInfo_LoadMap(getmapname_stored, reinit);
162 }
163
164 // return codes of map selectors:
165 //   -1 = temporary failure (that is, try some method that is guaranteed to succeed)
166 //   -2 = permanent failure
167 float MaplistMethod_Iterate() // usual method
168 {
169         float pass, i;
170
171         LOG_TRACE("Trying MaplistMethod_Iterate");
172
173         for(pass = 1; pass <= 2; ++pass)
174         {
175                 for(i = 1; i < Map_Count; ++i)
176                 {
177                         float mapindex;
178                         mapindex = (i + Map_Current) % Map_Count;
179                         if(Map_Check(mapindex, pass))
180                                 return mapindex;
181                 }
182         }
183         return -1;
184 }
185
186 float MaplistMethod_Repeat() // fallback method
187 {
188         LOG_TRACE("Trying MaplistMethod_Repeat");
189
190         if(Map_Check(Map_Current, 2))
191                 return Map_Current;
192         return -2;
193 }
194
195 float MaplistMethod_Random() // random map selection
196 {
197         float i, imax;
198
199         LOG_TRACE("Trying MaplistMethod_Random");
200
201         imax = 42;
202
203         for(i = 0; i <= imax; ++i)
204         {
205                 float mapindex;
206                 mapindex = (Map_Current + floor(random() * (Map_Count - 1) + 1)) % Map_Count; // any OTHER map
207                 if(Map_Check(mapindex, 1))
208                         return mapindex;
209         }
210         return -1;
211 }
212
213 // the exponent sets a bias on the map selection:
214 // the higher the exponent, the less likely "shortly repeated" same maps are
215 float MaplistMethod_Shuffle(float exponent) // more clever shuffling
216 {
217         float i, j, imax, insertpos;
218
219         LOG_TRACE("Trying MaplistMethod_Shuffle");
220
221         imax = 42;
222
223         for(i = 0; i <= imax; ++i)
224         {
225                 string newlist;
226
227                 // now reinsert this at another position
228                 insertpos = (random() ** (1 / exponent));       // ]0, 1]
229                 insertpos = insertpos * (Map_Count - 1);       // ]0, Map_Count - 1]
230                 insertpos = ceil(insertpos) + 1;               // {2, 3, 4, ..., Map_Count}
231                 LOG_TRACE("SHUFFLE: insert pos = ", ftos(insertpos));
232
233                 // insert the current map there
234                 newlist = "";
235                 for(j = 1; j < insertpos; ) // i == 1: no loop, will be inserted as first; however, i == 1 has been excluded above
236                 {
237                         if (j + 2 < insertpos)
238                                 newlist = strcat(newlist, " ", argv(j++), " ", argv(j++), " ", argv(j++));
239                         else
240                                 newlist = strcat(newlist, " ", argv(j++));
241                 }
242                 newlist = strcat(newlist, " ", argv(0));       // now insert the just selected map
243                 for(j = insertpos; j < Map_Count; ) // i == Map_Count: no loop, has just been inserted as last
244                 {
245                         if (j + 2 < Map_Count)
246                                 newlist = strcat(newlist, " ", argv(j++), " ", argv(j++), " ", argv(j++));
247                         else
248                                 newlist = strcat(newlist, " ", argv(j++));
249                 }
250                 newlist = substring(newlist, 1, strlen(newlist) - 1);
251                 cvar_set("g_maplist", newlist);
252                 Map_Count = tokenizebyseparator(autocvar_g_maplist, " ");
253
254                 // NOTE: the selected map has just been inserted at (insertpos-1)th position
255                 Map_Current = insertpos - 1; // this is not really valid, but this way the fallback has a chance of working
256                 if(Map_Check(Map_Current, 1))
257                         return Map_Current;
258         }
259         return -1;
260 }
261
262 void Maplist_Init()
263 {
264         float i = Map_Count = 0;
265         if(autocvar_g_maplist != "")
266         {
267                 Map_Count = tokenizebyseparator(autocvar_g_maplist, " ");
268                 for (i = 0; i < Map_Count; ++i)
269                 {
270                         if (Map_Check(i, 2))
271                                 break;
272                 }
273         }
274
275         if (i == Map_Count)
276         {
277                 bprint( "Maplist contains no usable maps!  Resetting it to default map list.\n" );
278                 cvar_set("g_maplist", MapInfo_ListAllAllowedMaps(MapInfo_RequiredFlags(), MapInfo_ForbiddenFlags() | MAPINFO_FLAG_NOAUTOMAPLIST));
279                 if(autocvar_g_maplist_shuffle)
280                         ShuffleMaplist();
281                 if(!server_is_dedicated)
282                         localcmd("\nmenu_cmd sync\n");
283                 Map_Count = tokenizebyseparator(autocvar_g_maplist, " ");
284         }
285         if(Map_Count == 0)
286                 error("empty maplist, cannot select a new map");
287         Map_Current = bound(0, GetMaplistPosition(), Map_Count - 1);
288
289         strcpy(Map_Current_Name, argv(Map_Current)); // will be automatically freed on exit thanks to DP
290         // this may or may not be correct, but who cares, in the worst case a map
291         // isn't chosen in the first pass that should have been
292 }
293
294 string GetNextMap()
295 {
296         Maplist_Init();
297         float nextMap = -1;
298
299         if(nextMap == -1)
300                 if(autocvar_g_maplist_shuffle > 0)
301                         nextMap = MaplistMethod_Shuffle(autocvar_g_maplist_shuffle + 1);
302
303         if(nextMap == -1)
304                 if(autocvar_g_maplist_selectrandom)
305                         nextMap = MaplistMethod_Random();
306
307         if(nextMap == -1)
308                 nextMap = MaplistMethod_Iterate();
309
310         if(nextMap == -1)
311                 nextMap = MaplistMethod_Repeat();
312
313         if(nextMap >= 0)
314         {
315                 Map_Goto_SetFloat(nextMap);
316                 return getmapname_stored;
317         }
318
319         return "";
320 }
321
322 float DoNextMapOverride(float reinit)
323 {
324         if(autocvar_g_campaign)
325         {
326                 CampaignPostIntermission();
327                 alreadychangedlevel = true;
328                 return true;
329         }
330         if(autocvar_quit_when_empty)
331         {
332                 if(player_count <= currentbots)
333                 {
334                         localcmd("quit\n");
335                         alreadychangedlevel = true;
336                         return true;
337                 }
338         }
339         if(autocvar_quit_and_redirect != "")
340         {
341                 redirection_target = strzone(autocvar_quit_and_redirect);
342                 alreadychangedlevel = true;
343                 return true;
344         }
345         if (!reinit && autocvar_samelevel) // if samelevel is set, stay on same level
346         {
347                 localcmd("restart\n");
348                 alreadychangedlevel = true;
349                 return true;
350         }
351         if(autocvar_nextmap != "")
352         {
353                 string m;
354                 m = GameTypeVote_MapInfo_FixName(autocvar_nextmap);
355                 cvar_set("nextmap",m);
356
357                 if(!m || gametypevote)
358                         return false;
359                 if(autocvar_sv_vote_gametype)
360                 {
361                         Map_Goto_SetStr(m);
362                         return false;
363                 }
364
365                 if(MapInfo_CheckMap(m))
366                 {
367                         Map_Goto_SetStr(m);
368                         Map_Goto(reinit);
369                         alreadychangedlevel = true;
370                         return true;
371                 }
372         }
373         if(!reinit && autocvar_lastlevel)
374         {
375                 cvar_settemp_restore();
376                 localcmd("set lastlevel 0\ntogglemenu 1\n");
377                 alreadychangedlevel = true;
378                 return true;
379         }
380         return false;
381 }
382
383 void GotoNextMap(float reinit)
384 {
385         //string nextmap;
386         //float n, nummaps;
387         //string s;
388         if (alreadychangedlevel)
389                 return;
390         alreadychangedlevel = true;
391
392         string nextMap = GetNextMap();
393         if(nextMap == "")
394                 error("Everything is broken - cannot find a next map. Please report this to the developers.");
395         Map_Goto(reinit);
396 }
397
398 void ShuffleMaplist()
399 {
400         cvar_set("g_maplist", shufflewords(autocvar_g_maplist));
401 }
402
403 string GotoMap(string m)
404 {
405         m = GameTypeVote_MapInfo_FixName(m);
406         if (!m)
407                 return "The map you suggested is not available on this server.";
408         if (!autocvar_sv_vote_gametype)
409         if(!MapInfo_CheckMap(m))
410                 return "The map you suggested does not support the current game mode.";
411         cvar_set("nextmap", m);
412         if (!intermission_running)
413                 cvar_set("_endmatch", "1");
414         if(mapvote_initialized || alreadychangedlevel)
415         {
416                 if(DoNextMapOverride(0))
417                         return "Map switch initiated.";
418                 else
419                         return "Hm... no. For some reason I like THIS map more.";
420         }
421         else
422                 return "Map switch will happen after scoreboard.";
423 }
424
425
426 /*
427 ============
428 IntermissionThink
429
430 When the player presses attack or jump, change to the next level
431 ============
432 */
433 .float autoscreenshot;
434 void IntermissionThink(entity this)
435 {
436         FixIntermissionClient(this);
437
438         float server_screenshot = (autocvar_sv_autoscreenshot && CS_CVAR(this).cvar_cl_autoscreenshot);
439         float client_screenshot = (CS_CVAR(this).cvar_cl_autoscreenshot == 2);
440
441         if( (server_screenshot || client_screenshot)
442                 && ((this.autoscreenshot > 0) && (time > this.autoscreenshot)) )
443         {
444                 this.autoscreenshot = -1;
445                 if(IS_REAL_CLIENT(this))
446                 {
447                         string num = strftime_s(); // strftime(false, "%s") isn't reliable, see strftime_s description
448                         stuffcmd(this, sprintf("\nscreenshot screenshots/autoscreenshot/%s-%s.jpg; "
449                                 "echo \"^5A screenshot has been taken at request of the server.\"\n", GetMapname(), num));
450                 }
451                 return;
452         }
453
454         if (time < intermission_exittime)
455                 return;
456
457         if(!mapvote_initialized)
458                 if (time < intermission_exittime + 10 && !(PHYS_INPUT_BUTTON_ATCK(this) || PHYS_INPUT_BUTTON_JUMP(this) || PHYS_INPUT_BUTTON_ATCK2(this) || PHYS_INPUT_BUTTON_HOOK(this) || PHYS_INPUT_BUTTON_USE(this)))
459                         return;
460
461         MapVote_Start();
462 }
463
464 void FixIntermissionClient(entity e)
465 {
466         if(!e.autoscreenshot) // initial call
467         {
468                 e.autoscreenshot = time + 0.8;  // used for autoscreenshot
469                 SetResourceExplicit(e, RES_HEALTH, -2342); // health in the first intermission phase
470                 for (int slot = 0; slot < MAX_WEAPONSLOTS; ++slot)
471                 {
472                         .entity weaponentity = weaponentities[slot];
473                         if(e.(weaponentity))
474                         {
475                                 e.(weaponentity).effects = EF_NODRAW;
476                                 if (e.(weaponentity).weaponchild)
477                                         e.(weaponentity).weaponchild.effects = EF_NODRAW;
478                         }
479                 }
480                 if(IS_REAL_CLIENT(e))
481                 {
482                         stuffcmd(e, "\nscr_printspeed 1000000\n");
483                         RandomSelection_Init();
484                         FOREACH_WORD(autocvar_sv_intermission_cdtrack, true, {
485                                 RandomSelection_AddString(it, 1, 1);
486                         });
487                         if (RandomSelection_chosen_string != "")
488                         {
489                                 stuffcmd(e, sprintf("\ncd loop %s\n", RandomSelection_chosen_string));
490                         }
491                         msg_entity = e;
492                         WriteByte(MSG_ONE, SVC_INTERMISSION);
493                 }
494         }
495 }