]> icculus.org git repositories - divverent/nexuiz.git/blob - data/qcsrc/common/mapinfo.qc
handle aborting when a mapinfo has been generated in a better way
[divverent/nexuiz.git] / data / qcsrc / common / mapinfo.qc
1 // HUGE SET - stored in a string
2 string HugeSetOfIntegers_empty()
3 {
4         return "";
5 }
6 float HugeSetOfIntegers_get(string pArr, float i)
7 {
8         return stof(substring(pArr, i * 4, 4));
9 }
10 float HugeSetOfIntegers_length(string pArr)
11 {
12         return strlen(pArr) / 4;
13 }
14 string HugeSetOfIntegers_concat(string a1, string a2)
15 {
16         return strcat(a1, a2);
17 }
18 string HugeSetOfIntegers_insert(string a1, float n, string a2)
19         // special concat function to build up large lists in less time by binary concatenation
20 {
21         string s;
22         s = strcat("    ", ftos(n));
23         return strcat(a1, substring(s, strlen(s) - 4, 4), a2);
24 }
25
26 // generic string stuff
27 float startsWith(string haystack, string needle)
28 {
29         return substring(haystack, 0, strlen(needle)) == needle;
30 }
31 string extractRestOfLine(string haystack, string needle)
32 {
33         if(startsWith(haystack, needle))
34                 return substring(haystack, strlen(needle), strlen(haystack) - strlen(needle));
35         return string_null;
36 }
37 string car(string s)
38 {
39         float o;
40         o = strstrofs(s, " ", 0);
41         if(o < 0)
42                 return s;
43         return substring(s, 0, o);
44 }
45 string cdr(string s)
46 {
47         float o;
48         o = strstrofs(s, " ", 0);
49         if(o < 0)
50                 return string_null;
51         return substring(s, o + 1, strlen(s) - (o + 1));
52 }
53
54 // GLOB HANDLING (for all BSP files)
55 float _MapInfo_globopen;
56 float _MapInfo_globcount; 
57 float _MapInfo_globhandle;
58 string _MapInfo_GlobItem(float i)
59 {
60         string s;
61         s = search_getfilename(_MapInfo_globhandle, i);
62         return substring(s, 5, strlen(s) - 9); // without maps/ and .bsp
63 }
64
65 void MapInfo_Enumerate()
66 {
67         if(_MapInfo_globopen)
68                 search_end(_MapInfo_globhandle);
69         _MapInfo_globhandle = search_begin("maps/*.bsp", TRUE, TRUE);
70         _MapInfo_globcount = search_getsize(_MapInfo_globhandle);
71         _MapInfo_globopen = 1;
72 }
73
74 // filter the info by game type mask (updates MapInfo_count)
75 string _MapInfo_filtered;
76 string MapInfo_FilterGametype_Recursive(float pGametype, float pFeatures, float pBegin, float pEnd, float pAbortOnGenerate)
77 {
78         float m, valid;
79         string l, r;
80
81         if(pBegin == pEnd)
82                 return HugeSetOfIntegers_empty();
83
84         m = floor((pBegin + pEnd) / 2);
85
86         l = MapInfo_FilterGametype_Recursive(pGametype, pFeatures, pBegin, m, pAbortOnGenerate);
87         if not(l)
88                 return string_null; // BAIL OUT
89         if(MapInfo_Get_ByName(_MapInfo_GlobItem(m), 1, 0) == 2) // if we generated one... BAIL OUT and let the caller continue in the next frame.
90                 if(pAbortOnGenerate)
91                         return string_null; // BAIL OUT
92         valid = (((MapInfo_Map_supportedGametypes & pGametype) != 0) && ((MapInfo_Map_supportedFeatures & pFeatures) == pFeatures));
93         r = MapInfo_FilterGametype_Recursive(pGametype, pFeatures, m + 1, pEnd, pAbortOnGenerate);
94         if not(r)
95                 return string_null; // BAIL OUT
96
97         if(valid)
98                 return HugeSetOfIntegers_insert(l, m, r);
99         else
100                 return HugeSetOfIntegers_concat(l, r);
101 }
102 float MapInfo_FilterGametype(float pGametype, float pFeatures, float pAbortOnGenerate)
103 {
104         if(_MapInfo_filtered)
105                 strunzone(_MapInfo_filtered);
106         _MapInfo_filtered = MapInfo_FilterGametype_Recursive(pGametype, pFeatures, 0, _MapInfo_globcount, pAbortOnGenerate);
107         if not(_MapInfo_filtered)
108         {
109                 dprint("Autogenerated a .mapinfo, doing the rest later.\n");
110                 return 0;
111         }
112         _MapInfo_filtered = strzone(_MapInfo_filtered);
113         MapInfo_count = HugeSetOfIntegers_length(_MapInfo_filtered);
114         //print("Filter ", ftos(pGametype), "/", ftos(pFeatures), " has ", ftos(MapInfo_count), "\n");
115         // TODO clear cache
116         return 1;
117 }
118
119 // load info about the i-th map into the MapInfo_Map_* globals
120 string MapInfo_BSPName_ByID(float i)
121 {
122         return _MapInfo_GlobItem(HugeSetOfIntegers_get(_MapInfo_filtered, i));
123 }
124
125 float MapInfo_Get_ByID(float i)
126 {
127         // TODO check cache
128         if(MapInfo_Get_ByName(MapInfo_BSPName_ByID(i), 0, 0))
129         {
130                 // TODO save in cache
131                 return 1;
132         }
133         return 0;
134 }
135
136 float _MapInfo_Generate(string pFilename) // 0: failure, 1: ok ent, 2: ok bsp
137 {
138         string fn;
139         float fh;
140         string s, v;
141         vector o;
142         float i;
143         float inWorldspawn, l;
144         float r;
145         float twoBaseModes;
146
147         vector mapMins, mapMaxs;
148
149         r = 1;
150         fn = strcat("maps/", pFilename, ".ent");
151         fh = fopen(fn, FILE_READ);
152         if(fh < 0)
153         {
154                 r = 2;
155                 fn = strcat("maps/", pFilename, ".bsp");
156                 fh = fopen(fn, FILE_READ);
157         }
158         if(fh < 0)
159                 return 0;
160         print("Analyzing ", fn, " to generate initial mapinfo; please edit that file later\n");
161
162         inWorldspawn = 2;
163         MapInfo_Map_supportedGametypes = 0;
164
165         for(;;)
166         {
167                 if not((s = fgets(fh)))
168                         break;
169                 if(inWorldspawn == 1)
170                         if(startsWith(s, "}"))
171                                 inWorldspawn = 0;
172                 if(inWorldspawn)
173                 {
174                         if(startsWith(s, "\"classname\" \"worldspawn\""))
175                                 inWorldspawn = 1;
176                         else if((v = extractRestOfLine(s, "\"author\" \"")))
177                         {
178                                 for(l = strlen(v) - 1; l > 0; --l)
179                                         if(substring(v, l, 1) == "\"")
180                                                 break;
181                                 MapInfo_Map_author = substring(v, 0, l);
182                         }
183                         else if((v = extractRestOfLine(s, "\"message\" \"")))
184                         {
185                                 for(l = strlen(v) - 1; l > 0; --l)
186                                         if(substring(v, l, 1) == "\"")
187                                                 break;
188                                 i = strstrofs(substring(v, 0, l), " by ", 0);
189                                 if(MapInfo_Map_author == "He-Who-Must-Not-Be-Named" && i >= 0)
190                                 {
191                                         MapInfo_Map_title = substring(v, 0, i);
192                                         MapInfo_Map_author = substring(v, i + 4, l - (i + 4));
193                                 }
194                                 else
195                                         MapInfo_Map_title = substring(v, 0, l);
196                         }
197                 }
198                 else
199                 {
200                         if((v = extractRestOfLine(s, "\"origin\" \"")))
201                         {
202                                 for(l = strlen(v) - 1; l > 0; --l)
203                                         if(substring(v, l, 1) == "\"")
204                                                 break;
205                                 o = stov(strcat("'", substring(v, 0, l), "'"));
206                                 mapMins_x = min(mapMins_x, o_x);
207                                 mapMins_y = min(mapMins_y, o_y);
208                                 mapMins_z = min(mapMins_z, o_z);
209                                 mapMaxs_x = max(mapMaxs_x, o_x);
210                                 mapMaxs_y = max(mapMaxs_y, o_y);
211                                 mapMaxs_z = max(mapMaxs_z, o_z);
212                         }
213                         else if((v = extractRestOfLine(s, "\"classname\" \"")))
214                         {
215                                 if(startsWith(v, "dom_controlpoint\""))
216                                         MapInfo_Map_supportedGametypes |= MAPINFO_TYPE_DOMINATION;
217                                 else if(startsWith(v, "item_flag_team2\""))
218                                         MapInfo_Map_supportedGametypes |= MAPINFO_TYPE_CTF;
219                                 else if(startsWith(v, "runematch_spawn_point\""))
220                                         MapInfo_Map_supportedGametypes |= MAPINFO_TYPE_RUNEMATCH;
221                                 else if(startsWith(v, "target_assault_roundend\""))
222                                         MapInfo_Map_supportedGametypes |= MAPINFO_TYPE_ASSAULT;
223                                 else if(startsWith(v, "onslaught_generator\""))
224                                         MapInfo_Map_supportedGametypes |= MAPINFO_TYPE_ONSLAUGHT;
225                                 else if(startsWith(v, "info_player_team1\""))
226                                         ++MapInfo_Map_spawnpoints;
227                                 else if(startsWith(v, "info_player_team2\""))
228                                         ++MapInfo_Map_spawnpoints;
229                                 else if(startsWith(v, "info_player_deathmatch\""))
230                                         ++MapInfo_Map_spawnpoints;
231                                 else if(startsWith(v, "info_player_start\""))
232                                         ++MapInfo_Map_spawnpoints;
233                                 else if(startsWith(v, "weapon_") && !startsWith(v, "weapon_nex\"") && !startsWith(v, "weapon_railgun\""))
234                                         MapInfo_Map_supportedFeatures |= MAPINFO_FEATURE_WEAPONS;
235                         }
236                 }
237         }
238         if(inWorldspawn)
239         {
240                 print(fn, " ended still in worldspawn, BUG\n");
241                 return 0;
242         }
243         MapInfo_Map_diameter = vlen(mapMaxs - mapMins);
244
245         twoBaseModes = MapInfo_Map_supportedGametypes & (MAPINFO_TYPE_CTF | MAPINFO_TYPE_ASSAULT);
246         if(twoBaseModes && (MapInfo_Map_supportedGametypes == twoBaseModes))
247         {
248                 // we have a CTF-only or Assault-only map. Don't add other modes then,
249                 // as the map is too symmetric for them.
250         }
251         else
252         {
253                 MapInfo_Map_supportedGametypes |= MAPINFO_TYPE_DEATHMATCH;      // DM always works
254                 MapInfo_Map_supportedGametypes |= MAPINFO_TYPE_LMS;             // LMS always works
255
256                 if(MapInfo_Map_spawnpoints >= 8  && MapInfo_Map_diameter > 4096)
257                         MapInfo_Map_supportedGametypes |= MAPINFO_TYPE_TEAM_DEATHMATCH;
258                 if(                MapInfo_Map_diameter < 4096)
259                         MapInfo_Map_supportedGametypes |= MAPINFO_TYPE_ARENA;
260                 if(MapInfo_Map_spawnpoints >= 12 && MapInfo_Map_diameter > 5120)
261                         MapInfo_Map_supportedGametypes |= MAPINFO_TYPE_KEYHUNT;
262         }
263
264         fclose(fh);
265         return r;
266 }
267
268 void _MapInfo_Map_Reset()
269 {
270         MapInfo_Map_title = "Untitled1";
271         MapInfo_Map_description = "Bleh.";
272         MapInfo_Map_author = "He-Who-Must-Not-Be-Named";
273         MapInfo_Map_supportedGametypes = 0;
274         MapInfo_Map_supportedFeatures = 0;
275         MapInfo_Map_diameter = 0;
276         MapInfo_Map_spawnpoints = 0;
277 }
278
279 void _MapInfo_Map_ApplyGametype(string s, float pWantedType, float pThisType)
280 {
281         MapInfo_Map_supportedGametypes |= pThisType;
282         if(!(pThisType & pWantedType))
283                 return;
284         
285         cvar_set("fraglimit", car(s));
286         s = cdr(s);
287
288         cvar_set("timelimit", car(s));
289         s = cdr(s);
290
291         if(pWantedType == MAPINFO_TYPE_TEAM_DEATHMATCH)
292         {
293                 cvar_set("g_tdm_teams", car(s));
294                 s = cdr(s);
295         }
296
297         if(pWantedType == MAPINFO_TYPE_KEYHUNT)
298         {
299                 cvar_set("g_keyhunt_teams", car(s));
300                 s = cdr(s);
301         }
302 }
303
304 float MapInfo_Type_FromString(string t)
305 {
306         if     (t == "dm")    return MAPINFO_TYPE_DEATHMATCH;
307         else if(t == "tdm")   return MAPINFO_TYPE_TEAM_DEATHMATCH;
308         else if(t == "dom")   return MAPINFO_TYPE_DOMINATION;
309         else if(t == "ctf")   return MAPINFO_TYPE_CTF;
310         else if(t == "rune")  return MAPINFO_TYPE_RUNEMATCH;
311         else if(t == "lms")   return MAPINFO_TYPE_LMS;
312         else if(t == "arena") return MAPINFO_TYPE_ARENA;
313         else if(t == "kh")    return MAPINFO_TYPE_KEYHUNT;
314         else if(t == "as")    return MAPINFO_TYPE_ASSAULT;
315         else if(t == "ons")   return MAPINFO_TYPE_ONSLAUGHT;
316         else                  return 0;
317 }
318
319 // load info about a map by name into the MapInfo_Map_* globals
320 float MapInfo_Get_ByName(string pFilename, float pAllowGenerate, float pGametypeToSet)
321 {
322         string fn;
323         string s, t;
324         float fh;
325         float r, f;
326
327         r = 1;
328
329         MapInfo_Map_bspname = pFilename;
330
331         // default all generic fields so they have "good" values in case something fails
332         fn = strcat("maps/", pFilename, ".mapinfo");
333         fh = fopen(fn, FILE_READ);
334         if(fh < 0)
335         {
336                 if(!pAllowGenerate)
337                         return 0;
338                 _MapInfo_Map_Reset();
339                 r = _MapInfo_Generate(pFilename);
340                 if(!r)
341                         return 0;
342                 fh = fopen(fn, FILE_WRITE);
343                 fputs(fh, strcat("title ", MapInfo_Map_title, "\n"));
344                 fputs(fh, strcat("description ", MapInfo_Map_description, "\n"));
345                 fputs(fh, strcat("author ", MapInfo_Map_author, "\n"));
346                 fputs(fh, strcat("_diameter ", ftos(MapInfo_Map_diameter), "\n"));
347                 fputs(fh, strcat("_spawnpoints ", ftos(MapInfo_Map_spawnpoints), "\n"));
348                 if(MapInfo_Map_supportedFeatures & MAPINFO_FEATURE_WEAPONS)       fputs(fh, "has weapons\n");
349                 if(MapInfo_Map_supportedGametypes & MAPINFO_TYPE_DEATHMATCH)      fputs(fh, "type dm 30 20\n");
350                 if(MapInfo_Map_supportedGametypes & MAPINFO_TYPE_TEAM_DEATHMATCH) fputs(fh, "type tdm 50 20 2\n");
351                 if(MapInfo_Map_supportedGametypes & MAPINFO_TYPE_DOMINATION)      fputs(fh, "type dom 200 20\n");
352                 if(MapInfo_Map_supportedGametypes & MAPINFO_TYPE_CTF)             fputs(fh, "type ctf 300 20\n");
353                 if(MapInfo_Map_supportedGametypes & MAPINFO_TYPE_RUNEMATCH)       fputs(fh, "type rune 200 20\n");
354                 if(MapInfo_Map_supportedGametypes & MAPINFO_TYPE_LMS)             fputs(fh, "type lms 9 20\n");
355                 if(MapInfo_Map_supportedGametypes & MAPINFO_TYPE_ARENA)           fputs(fh, "type arena 10 20\n");
356                 if(MapInfo_Map_supportedGametypes & MAPINFO_TYPE_KEYHUNT)         fputs(fh, "type kh 1000 20 3\n");
357                 if(MapInfo_Map_supportedGametypes & MAPINFO_TYPE_ASSAULT)         fputs(fh, "type as 20\n");
358                 if(MapInfo_Map_supportedGametypes & MAPINFO_TYPE_ONSLAUGHT)       fputs(fh, "type ons 20\n");
359                 fclose(fh);
360                 r = 2;
361                 // return r;
362                 fh = fopen(fn, FILE_READ);
363                 if(fh < 0)
364                         error("... but I just wrote it!");
365         }
366
367         _MapInfo_Map_Reset();
368         for(;;)
369         {
370                 if not((s = fgets(fh)))
371                         break;
372                 t = car(s); s = cdr(s);
373                 if     (t == "title")
374                         MapInfo_Map_title = s;
375                 else if(t == "description")
376                         MapInfo_Map_description = s;
377                 else if(t == "author")
378                         MapInfo_Map_author = s;
379                 else if(t == "_diameter")
380                         MapInfo_Map_diameter = stof(s);
381                 else if(t == "_spawnpoints")
382                         MapInfo_Map_spawnpoints = stof(s);
383                 else if(t == "has")
384                 {
385                         t = car(s); s = cdr(s);
386                         if     (t == "weapons") MapInfo_Map_supportedFeatures |= MAPINFO_FEATURE_WEAPONS;
387                         else
388                                 dprint("Map ", pFilename, " supports unknown feature ", t, ", ignored\n");
389                 }
390                 else if(t == "type")
391                 {
392                         t = car(s); s = cdr(s);
393                         f = MapInfo_Type_FromString(t);
394                         if(f)
395                                 _MapInfo_Map_ApplyGametype (s, pGametypeToSet, f);
396                         else
397                                 dprint("Map ", pFilename, " supports unknown game type ", t, ", ignored\n");
398                 }
399                 else
400                         dprint("Map ", pFilename, " provides unknown info item ", t, ", ignored\n");
401         }
402         fclose(fh);
403         if(pGametypeToSet)
404                 if(!(MapInfo_Map_supportedGametypes & pGametypeToSet))
405                         error("Can't select the requested game type. Bailing out.");
406         if(MapInfo_Map_supportedGametypes != 0)
407                 return r;
408         dprint("Map ", pFilename, " supports no game types, ignored\n");
409         return 0;
410 }
411
412 string _MapInfo_FindName_match;
413 float MapInfo_FindName(string s)
414 {
415         // if there is exactly one map of prefix s, return it
416         // if not, return the null string
417         // note that DP sorts glob results... so I can use a binary search
418         float l, r, m, cmp;
419         l = 0;
420         r = MapInfo_count;
421         _MapInfo_FindName_match = string_null;
422         // invariants: r is behind s, l-1 is equal or before
423         while(l != r)
424         {
425                 m = floor((l + r) / 2);
426                 cmp = strcasecmp(_MapInfo_GlobItem(HugeSetOfIntegers_get(_MapInfo_filtered, m)), s);
427                 if(cmp == 0)
428                         return m; // found and good
429                 if(cmp < 0)
430                         l = m + 1; // l-1 is before s
431                 else
432                         r = m; // behind s
433         }
434         // r == l, so: l is behind s, l-1 is before
435         // SO: if there is any, l is the one with the right prefix
436         //     and l+1 may be one too
437         if(l == MapInfo_count)
438                 return -1; // no _MapInfo_FindName_match, behind last item
439         _MapInfo_FindName_match = _MapInfo_GlobItem(HugeSetOfIntegers_get(_MapInfo_filtered, l));
440         if(!startsWith(_MapInfo_FindName_match, s))
441                 return -1; // wrong prefix
442         if(l == MapInfo_count - 1)
443                 return l; // last one, nothing can follow => unique
444         if(startsWith(_MapInfo_GlobItem(HugeSetOfIntegers_get(_MapInfo_filtered, l + 1)), s))
445                 return -1; // ambigous _MapInfo_FindName_match
446         return l;
447 }
448
449 string MapInfo_FixName(string s)
450 {
451         MapInfo_FindName(s);
452         return _MapInfo_FindName_match;
453 }
454
455 float MapInfo_CurrentFeatures()
456 {
457         float req;
458         req = 0;
459         if(!(cvar("g_instagib") || cvar("g_minstagib") || cvar("g_nixnex") || cvar("g_rocketarena")))
460                 req |= MAPINFO_FEATURE_WEAPONS;
461         return req;
462 }
463
464 float MapInfo_CurrentGametype()
465 {
466         if(cvar("g_domination"))
467                 return MAPINFO_TYPE_DOMINATION;
468         else if(cvar("g_ctf"))
469                 return MAPINFO_TYPE_CTF;
470         else if(cvar("g_runematch"))
471                 return MAPINFO_TYPE_RUNEMATCH;
472         else if(cvar("g_tdm"))
473                 return MAPINFO_TYPE_TEAM_DEATHMATCH;
474         else if(cvar("g_assault"))
475                 return MAPINFO_TYPE_ASSAULT;
476         else if(cvar("g_lms"))
477                 return MAPINFO_TYPE_LMS;
478         else if(cvar("g_arena"))
479                 return MAPINFO_TYPE_ARENA;
480         else if(cvar("g_keyhunt"))
481                 return MAPINFO_TYPE_KEYHUNT;
482         else if(cvar("g_onslaught"))
483                 return MAPINFO_TYPE_ONSLAUGHT;
484         else
485                 return MAPINFO_TYPE_DEATHMATCH;
486 }
487
488 float MapInfo_CheckMap(string s) // returns 0 if the map can't be played with the current settings, 1 otherwise
489 {
490         if(!MapInfo_Get_ByName(s, 1, 0))
491                 return 0;
492         if((MapInfo_Map_supportedGametypes & MapInfo_CurrentGametype()) == 0)
493                 return 0;
494         if((MapInfo_Map_supportedFeatures & MapInfo_CurrentFeatures()) != MapInfo_CurrentFeatures())
495                 return 0;
496         return 1;
497 }
498
499 void MapInfo_SwitchGameType(float t)
500 {
501         cvar_set("gamecfg",      "0");
502         cvar_set("g_dm",         (t == MAPINFO_TYPE_DEATHMATCH)      ? "0" : "1");
503         cvar_set("g_tdm",        (t == MAPINFO_TYPE_TEAM_DEATHMATCH) ? "0" : "1");
504         cvar_set("g_domination", (t == MAPINFO_TYPE_DOMINATION)      ? "0" : "1");
505         cvar_set("g_ctf",        (t == MAPINFO_TYPE_CTF)             ? "0" : "1");
506         cvar_set("g_runematch",  (t == MAPINFO_TYPE_RUNEMATCH)       ? "0" : "1");
507         cvar_set("g_lms",        (t == MAPINFO_TYPE_LMS)             ? "0" : "1");
508         cvar_set("g_arena",      (t == MAPINFO_TYPE_ARENA)           ? "0" : "1");
509         cvar_set("g_keyhunt",    (t == MAPINFO_TYPE_KEYHUNT)         ? "0" : "1");
510         cvar_set("g_assault",    (t == MAPINFO_TYPE_ASSAULT)         ? "0" : "1");
511         cvar_set("g_onslaught",  (t == MAPINFO_TYPE_ONSLAUGHT)       ? "0" : "1");
512 }
513
514 void MapInfo_LoadMap(string s)
515 {
516         if(!MapInfo_CheckMap(s))
517         {
518                 print("EMERGENCY: can't play the selected map in the given game mode. Falling back to deathmatch.\n");
519                 MapInfo_SwitchGameType(MAPINFO_TYPE_DEATHMATCH);
520         }
521         MapInfo_Get_ByName(s, 1, MapInfo_CurrentGametype());
522         localcmd(strcat("\nchangelevel ", s, "\n"));
523 }
524
525 string MapInfo_ListAllowedMaps()
526 {
527         string out;
528         float i;
529
530         // to make absolutely sure:
531         MapInfo_Enumerate();
532         MapInfo_FilterGametype(MapInfo_CurrentGametype(), MapInfo_CurrentFeatures(), 0);
533
534         out = "";
535         for(i = 0; i < MapInfo_count; ++i)
536                 out = strcat(out, " ", _MapInfo_GlobItem(HugeSetOfIntegers_get(_MapInfo_filtered, i)));
537         return substring(out, 1, strlen(out) - 1);
538 }