98d2ef25b08febaa7805ba1f42e5f7327cb9c0af
[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) * AvailableTeams());
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         string opensize_msg = strcat("opensize ", map);
75         float fh = fopen(strcat("maps/", map, ".sizes"), FILE_READ);
76         int player_limit = ((autocvar_g_maplist_sizes_count_maxplayers) ? GetPlayerLimit() : 0);
77         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
78         if(!autocvar_g_maplist_sizes_count_bots)
79                 pcount -= currentbots;
80         if(fh >= 0)
81         {
82                 opensize_msg = strcat(opensize_msg, ": ok, ");
83                 int mapmin = stoi(fgets(fh));
84                 int mapmax = stoi(fgets(fh));
85                 fclose(fh);
86                 if(pcount < mapmin)
87                 {
88                         LOG_TRACE(opensize_msg, "not enough");
89                         return false;
90                 }
91                 if(mapmax && pcount > mapmax)
92                 {
93                         LOG_TRACE(opensize_msg, "too many");
94                         return false;
95                 }
96                 LOG_TRACE(opensize_msg, "right size");
97                 return true;
98         }
99         LOG_TRACE(opensize_msg, ": not found");
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 float MaplistMethod_Shuffle(float exponent) // more clever shuffling
214 // the exponent sets a bias on the map selection:
215 // the higher the exponent, the less likely "shortly repeated" same maps are
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; ++j)                 // i == 1: no loop, will be inserted as first; however, i == 1 has been excluded above
236                         newlist = strcat(newlist, " ", argv(j));
237                 newlist = strcat(newlist, " ", argv(0));       // now insert the just selected map
238                 for(j = insertpos; j < Map_Count; ++j)         // i == Map_Count: no loop, has just been inserted as last
239                         newlist = strcat(newlist, " ", argv(j));
240                 newlist = substring(newlist, 1, strlen(newlist) - 1);
241                 cvar_set("g_maplist", newlist);
242                 Map_Count = tokenizebyseparator(autocvar_g_maplist, " ");
243
244                 // NOTE: the selected map has just been inserted at (insertpos-1)th position
245                 Map_Current = insertpos - 1; // this is not really valid, but this way the fallback has a chance of working
246                 if(Map_Check(Map_Current, 1))
247                         return Map_Current;
248         }
249         return -1;
250 }
251
252 void Maplist_Init()
253 {
254         float i = Map_Count = 0;
255         if(autocvar_g_maplist != "")
256         {
257                 Map_Count = tokenizebyseparator(autocvar_g_maplist, " ");
258                 for (i = 0; i < Map_Count; ++i)
259                 {
260                         if (Map_Check(i, 2))
261                                 break;
262                 }
263         }
264
265         if (i == Map_Count)
266         {
267                 bprint( "Maplist contains no usable maps!  Resetting it to default map list.\n" );
268                 cvar_set("g_maplist", MapInfo_ListAllAllowedMaps(MapInfo_RequiredFlags(), MapInfo_ForbiddenFlags() | MAPINFO_FLAG_NOAUTOMAPLIST));
269                 if(autocvar_g_maplist_shuffle)
270                         ShuffleMaplist();
271                 if(!server_is_dedicated)
272                         localcmd("\nmenu_cmd sync\n");
273                 Map_Count = tokenizebyseparator(autocvar_g_maplist, " ");
274         }
275         if(Map_Count == 0)
276                 error("empty maplist, cannot select a new map");
277         Map_Current = bound(0, GetMaplistPosition(), Map_Count - 1);
278
279         strcpy(Map_Current_Name, argv(Map_Current)); // will be automatically freed on exit thanks to DP
280         // this may or may not be correct, but who cares, in the worst case a map
281         // isn't chosen in the first pass that should have been
282 }
283
284 string GetNextMap()
285 {
286         Maplist_Init();
287         float nextMap = -1;
288
289         if(nextMap == -1)
290                 if(autocvar_g_maplist_shuffle > 0)
291                         nextMap = MaplistMethod_Shuffle(autocvar_g_maplist_shuffle + 1);
292
293         if(nextMap == -1)
294                 if(autocvar_g_maplist_selectrandom)
295                         nextMap = MaplistMethod_Random();
296
297         if(nextMap == -1)
298                 nextMap = MaplistMethod_Iterate();
299
300         if(nextMap == -1)
301                 nextMap = MaplistMethod_Repeat();
302
303         if(nextMap >= 0)
304         {
305                 Map_Goto_SetFloat(nextMap);
306                 return getmapname_stored;
307         }
308
309         return "";
310 }
311
312 float DoNextMapOverride(float reinit)
313 {
314         if(autocvar_g_campaign)
315         {
316                 CampaignPostIntermission();
317                 alreadychangedlevel = true;
318                 return true;
319         }
320         if(autocvar_quit_when_empty)
321         {
322                 if(player_count <= currentbots)
323                 {
324                         localcmd("quit\n");
325                         alreadychangedlevel = true;
326                         return true;
327                 }
328         }
329         if(autocvar_quit_and_redirect != "")
330         {
331                 redirection_target = strzone(autocvar_quit_and_redirect);
332                 alreadychangedlevel = true;
333                 return true;
334         }
335         if (!reinit && autocvar_samelevel) // if samelevel is set, stay on same level
336         {
337                 localcmd("restart\n");
338                 alreadychangedlevel = true;
339                 return true;
340         }
341         if(autocvar_nextmap != "")
342         {
343                 string m;
344                 m = GameTypeVote_MapInfo_FixName(autocvar_nextmap);
345                 cvar_set("nextmap",m);
346
347                 if(!m || gametypevote)
348                         return false;
349                 if(autocvar_sv_vote_gametype)
350                 {
351                         Map_Goto_SetStr(m);
352                         return false;
353                 }
354
355                 if(MapInfo_CheckMap(m))
356                 {
357                         Map_Goto_SetStr(m);
358                         Map_Goto(reinit);
359                         alreadychangedlevel = true;
360                         return true;
361                 }
362         }
363         if(!reinit && autocvar_lastlevel)
364         {
365                 cvar_settemp_restore();
366                 localcmd("set lastlevel 0\ntogglemenu 1\n");
367                 alreadychangedlevel = true;
368                 return true;
369         }
370         return false;
371 }
372
373 void GotoNextMap(float reinit)
374 {
375         //string nextmap;
376         //float n, nummaps;
377         //string s;
378         if (alreadychangedlevel)
379                 return;
380         alreadychangedlevel = true;
381
382         string nextMap = GetNextMap();
383         if(nextMap == "")
384                 error("Everything is broken - cannot find a next map. Please report this to the developers.");
385         Map_Goto(reinit);
386 }
387
388 void ShuffleMaplist()
389 {
390         cvar_set("g_maplist", shufflewords(autocvar_g_maplist));
391 }
392
393 string GotoMap(string m)
394 {
395         m = GameTypeVote_MapInfo_FixName(m);
396         if (!m)
397                 return "The map you suggested is not available on this server.";
398         if (!autocvar_sv_vote_gametype)
399         if(!MapInfo_CheckMap(m))
400                 return "The map you suggested does not support the current game mode.";
401         cvar_set("nextmap", m);
402         cvar_set("_endmatch", "1");
403         if(mapvote_initialized || alreadychangedlevel)
404         {
405                 if(DoNextMapOverride(0))
406                         return "Map switch initiated.";
407                 else
408                         return "Hm... no. For some reason I like THIS map more.";
409         }
410         else
411                 return "Map switch will happen after scoreboard.";
412 }
413
414
415 /*
416 ============
417 IntermissionThink
418
419 When the player presses attack or jump, change to the next level
420 ============
421 */
422 .float autoscreenshot;
423 void IntermissionThink(entity this)
424 {
425         FixIntermissionClient(this);
426
427         float server_screenshot = (autocvar_sv_autoscreenshot && CS_CVAR(this).cvar_cl_autoscreenshot);
428         float client_screenshot = (CS_CVAR(this).cvar_cl_autoscreenshot == 2);
429
430         if( (server_screenshot || client_screenshot)
431                 && ((this.autoscreenshot > 0) && (time > this.autoscreenshot)) )
432         {
433                 this.autoscreenshot = -1;
434                 if(IS_REAL_CLIENT(this))
435                 {
436                         string num = strftime_s(); // strftime(false, "%s") isn't reliable, see strftime_s description
437                         stuffcmd(this, sprintf("\nscreenshot screenshots/autoscreenshot/%s-%s.jpg; "
438                                 "echo \"^5A screenshot has been taken at request of the server.\"\n", GetMapname(), num));
439                 }
440                 return;
441         }
442
443         if (time < intermission_exittime)
444                 return;
445
446         if(!mapvote_initialized)
447                 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)))
448                         return;
449
450         MapVote_Start();
451 }
452
453 void FixIntermissionClient(entity e)
454 {
455         if(!e.autoscreenshot) // initial call
456         {
457                 e.autoscreenshot = time + 0.8;  // used for autoscreenshot
458                 SetResourceExplicit(e, RES_HEALTH, -2342); // health in the first intermission phase
459                 for (int slot = 0; slot < MAX_WEAPONSLOTS; ++slot)
460                 {
461                         .entity weaponentity = weaponentities[slot];
462                         if(e.(weaponentity))
463                         {
464                                 e.(weaponentity).effects = EF_NODRAW;
465                                 if (e.(weaponentity).weaponchild)
466                                         e.(weaponentity).weaponchild.effects = EF_NODRAW;
467                         }
468                 }
469                 if(IS_REAL_CLIENT(e))
470                 {
471                         stuffcmd(e, "\nscr_printspeed 1000000\n");
472                         RandomSelection_Init();
473                         FOREACH_WORD(autocvar_sv_intermission_cdtrack, true, {
474                                 RandomSelection_AddString(it, 1, 1);
475                         });
476                         if (RandomSelection_chosen_string != "")
477                         {
478                                 stuffcmd(e, sprintf("\ncd loop %s\n", RandomSelection_chosen_string));
479                         }
480                         msg_entity = e;
481                         WriteByte(MSG_ONE, SVC_INTERMISSION);
482                 }
483         }
484 }