]> de.git.xonotic.org Git - xonotic/xonotic-data.pk3dir.git/blob - qcsrc/server/miscfunctions.qc
7da78ca0212baa6274886ec256f69b53ec00a8b6
[xonotic/xonotic-data.pk3dir.git] / qcsrc / server / miscfunctions.qc
1 #include "miscfunctions.qh"
2
3 #include "antilag.qh"
4 #include "command/common.qh"
5 #include "client.qh"
6 #include "damage.qh"
7 #include "hook.qh"
8 #include "world.qh"
9 #include <server/gamelog.qh>
10 #include "ipban.qh"
11 #include <server/intermission.qh>
12 #include <server/items/items.qh>
13 #include <server/mutators/_mod.qh>
14 #include <server/spawnpoints.qh>
15 #include <server/main.qh>
16 #include "mapvoting.qh"
17 #include "resources.qh"
18 #include <server/items/spawning.qh>
19 #include "player.qh"
20 #include "weapons/accuracy.qh"
21 #include "weapons/common.qh"
22 #include "weapons/csqcprojectile.qh"
23 #include "weapons/selection.qh"
24 #include "../common/command/_mod.qh"
25 #include "../common/constants.qh"
26 #include <common/net_linked.qh>
27 #include <common/weapons/weapon/crylink.qh>
28 #include "../common/deathtypes/all.qh"
29 #include "../common/mapinfo.qh"
30 #include "../common/notifications/all.qh"
31 #include "../common/playerstats.qh"
32 #include "../common/teams.qh"
33 #include "../common/mapobjects/subs.qh"
34 #include <common/mapobjects/trigger/hurt.qh>
35 #include <common/mapobjects/target/location.qh>
36 #include "../common/util.qh"
37 #include "../common/turrets/sv_turrets.qh"
38 #include <common/weapons/_all.qh>
39 #include "../common/vehicles/sv_vehicles.qh"
40 #include "../common/vehicles/vehicle.qh"
41 #include "../common/items/_mod.qh"
42 #include "../common/state.qh"
43 #include "../common/effects/qc/globalsound.qh"
44 #include "../common/wepent.qh"
45 #include <common/weapons/weapon.qh>
46 #include "../lib/csqcmodel/sv_model.qh"
47 #include "../lib/warpzone/anglestransform.qh"
48 #include "../lib/warpzone/server.qh"
49
50 void crosshair_trace(entity pl)
51 {
52         traceline_antilag(pl, CS(pl).cursor_trace_start, CS(pl).cursor_trace_start + normalize(CS(pl).cursor_trace_endpos - CS(pl).cursor_trace_start) * max_shot_distance, MOVE_NORMAL, pl, ANTILAG_LATENCY(pl));
53 }
54
55 void crosshair_trace_plusvisibletriggers(entity pl)
56 {
57         crosshair_trace_plusvisibletriggers__is_wz(pl, false);
58 }
59
60 void WarpZone_crosshair_trace_plusvisibletriggers(entity pl)
61 {
62         crosshair_trace_plusvisibletriggers__is_wz(pl, true);
63 }
64
65 void crosshair_trace_plusvisibletriggers__is_wz(entity pl, bool is_wz)
66 {
67         FOREACH_ENTITY_FLOAT(solid, SOLID_TRIGGER,
68         {
69                 if(it.model != "")
70                 {
71                         it.solid = SOLID_BSP;
72                         IL_PUSH(g_ctrace_changed, it);
73                 }
74         });
75
76         if (is_wz)
77                 WarpZone_crosshair_trace(pl);
78         else
79                 crosshair_trace(pl);
80
81         IL_EACH(g_ctrace_changed, true, { it.solid = SOLID_TRIGGER; });
82
83         IL_CLEAR(g_ctrace_changed);
84 }
85
86 void WarpZone_crosshair_trace(entity pl)
87 {
88         WarpZone_traceline_antilag(pl, CS(pl).cursor_trace_start, CS(pl).cursor_trace_start + normalize(CS(pl).cursor_trace_endpos - CS(pl).cursor_trace_start) * max_shot_distance, MOVE_NORMAL, pl, ANTILAG_LATENCY(pl));
89 }
90
91 entity findnearest(vector point, bool checkitems, vector axismod)
92 {
93     vector dist;
94     int num_nearest = 0;
95
96     IL_EACH(((checkitems) ? g_items : g_locations), ((checkitems) ? (it.target == "###item###") : (it.classname == "target_location")),
97     {
98         if ((it.items == IT_KEY1 || it.items == IT_KEY2) && it.target == "###item###")
99             dist = it.oldorigin;
100         else
101             dist = it.origin;
102         dist = dist - point;
103         dist = dist.x * axismod.x * '1 0 0' + dist.y * axismod.y * '0 1 0' + dist.z * axismod.z * '0 0 1';
104         float len = vlen2(dist);
105
106         int l;
107         for (l = 0; l < num_nearest; ++l)
108         {
109             if (len < nearest_length[l])
110                 break;
111         }
112
113         // now i tells us where to insert at
114         //   INSERTION SORT! YOU'VE SEEN IT! RUN!
115         if (l < NUM_NEAREST_ENTITIES)
116         {
117             for (int j = NUM_NEAREST_ENTITIES - 1; j >= l; --j)
118             {
119                 nearest_length[j + 1] = nearest_length[j];
120                 nearest_entity[j + 1] = nearest_entity[j];
121             }
122             nearest_length[l] = len;
123             nearest_entity[l] = it;
124             if (num_nearest < NUM_NEAREST_ENTITIES)
125                 num_nearest = num_nearest + 1;
126         }
127     });
128
129     // now use the first one from our list that we can see
130     for (int j = 0; j < num_nearest; ++j)
131     {
132         traceline(point, nearest_entity[j].origin, true, NULL);
133         if (trace_fraction == 1)
134         {
135             if (j != 0)
136                 LOG_TRACEF("Nearest point (%s) is not visible, using a visible one.", nearest_entity[0].netname);
137             return nearest_entity[j];
138         }
139     }
140
141     if (num_nearest == 0)
142         return NULL;
143
144     LOG_TRACE("Not seeing any location point, using nearest as fallback.");
145     /* DEBUGGING CODE:
146     dprint("Candidates were: ");
147     for(j = 0; j < num_nearest; ++j)
148     {
149         if(j != 0)
150                 dprint(", ");
151         dprint(nearest_entity[j].netname);
152     }
153     dprint("\n");
154     */
155
156     return nearest_entity[0];
157 }
158
159 string NearestLocation(vector p)
160 {
161     string ret = "somewhere";
162     entity loc = findnearest(p, false, '1 1 1');
163     if (loc)
164         ret = loc.message;
165     else
166     {
167         loc = findnearest(p, true, '1 1 4');
168         if (loc)
169             ret = loc.netname;
170     }
171     return ret;
172 }
173
174 string PlayerHealth(entity this)
175 {
176         float myhealth = floor(GetResource(this, RES_HEALTH));
177         if(myhealth == -666)
178                 return "spectating";
179         else if(myhealth == -2342 || (myhealth == 2342 && mapvote_initialized))
180                 return "observing";
181         else if(myhealth <= 0 || IS_DEAD(this))
182                 return "dead";
183         return ftos(myhealth);
184 }
185
186 string WeaponNameFromWeaponentity(entity this, .entity weaponentity)
187 {
188         entity wepent = this.(weaponentity);
189         if(!wepent)
190                 return "none";
191         else if(wepent.m_weapon != WEP_Null)
192                 return wepent.m_weapon.m_name;
193         else if(wepent.m_switchweapon != WEP_Null)
194                 return wepent.m_switchweapon.m_name;
195         return "none"; //REGISTRY_GET(Weapons, wepent.cnt).m_name;
196 }
197
198 string formatmessage(entity this, string msg)
199 {
200         float p, p1, p2;
201         float n;
202         vector cursor = '0 0 0';
203         entity cursor_ent = NULL;
204         string escape;
205         string replacement;
206         p = 0;
207         n = 7;
208         bool traced = false;
209
210         MUTATOR_CALLHOOK(PreFormatMessage, this, msg);
211         msg = M_ARGV(1, string);
212
213         while (1) {
214                 if (n < 1)
215                         break; // too many replacements
216
217                 n = n - 1;
218                 p1 = strstrofs(msg, "%", p); // NOTE: this destroys msg as it's a tempstring!
219                 p2 = strstrofs(msg, "\\", p); // NOTE: this destroys msg as it's a tempstring!
220
221                 if (p1 < 0)
222                         p1 = p2;
223
224                 if (p2 < 0)
225                         p2 = p1;
226
227                 p = min(p1, p2);
228
229                 if (p < 0)
230                         break;
231
232                 if(!traced)
233                 {
234                         WarpZone_crosshair_trace_plusvisibletriggers(this);
235                         cursor = trace_endpos;
236                         cursor_ent = trace_ent;
237                         traced = true;
238                 }
239
240                 replacement = substring(msg, p, 2);
241                 escape = substring(msg, p + 1, 1);
242
243                 .entity weaponentity = weaponentities[0]; // TODO: unhardcode
244
245                 switch(escape)
246                 {
247                         case "%": replacement = "%"; break;
248                         case "\\":replacement = "\\"; break;
249                         case "n": replacement = "\n"; break;
250                         case "a": replacement = ftos(floor(GetResource(this, RES_ARMOR))); break;
251                         case "h": replacement = PlayerHealth(this); break;
252                         case "l": replacement = NearestLocation(this.origin); break;
253                         case "y": replacement = NearestLocation(cursor); break;
254                         case "d": replacement = NearestLocation(this.death_origin); break;
255                         case "w": replacement = WeaponNameFromWeaponentity(this, weaponentity); break;
256                         case "W": replacement = GetAmmoName(this.(weaponentity).m_weapon.ammo_type); break;
257                         case "x": replacement = ((cursor_ent.netname == "" || !cursor_ent) ? "nothing" : cursor_ent.netname); break;
258                         case "s": replacement = ftos(vlen(this.velocity - this.velocity_z * '0 0 1')); break;
259                         case "S": replacement = ftos(vlen(this.velocity)); break;
260                         case "t": replacement = seconds_tostring(ceil(max(0, autocvar_timelimit * 60 + game_starttime - time))); break;
261                         case "T": replacement = seconds_tostring(floor(time - game_starttime)); break;
262                         default:
263                         {
264                                 MUTATOR_CALLHOOK(FormatMessage, this, escape, replacement, msg);
265                                 replacement = M_ARGV(2, string);
266                                 break;
267                         }
268                 }
269
270                 msg = strcat(substring(msg, 0, p), replacement, substring(msg, p+2, strlen(msg) - (p+2)));
271                 p = p + strlen(replacement);
272         }
273         return msg;
274 }
275
276 /*
277 =============
278 GetCvars
279 =============
280 Called with:
281   0:  sends the request
282   >0: receives a cvar from name=argv(f) value=argv(f+1)
283 */
284 void GetCvars_handleString(entity this, entity store, string thisname, float f, .string field, string name)
285 {
286         if (f < 0)
287         {
288                 strfree(store.(field));
289         }
290         else if (f > 0)
291         {
292                 if (thisname == name)
293                 {
294                         strcpy(store.(field), argv(f + 1));
295                 }
296         }
297         else
298                 stuffcmd(this, strcat("cl_cmd sendcvar ", name, "\n"));
299 }
300 void GetCvars_handleString_Fixup(entity this, entity store, string thisname, float f, .string field, string name, string(entity, string) func)
301 {
302         GetCvars_handleString(this, store, thisname, f, field, name);
303         if (f >= 0) // also initialize to the fitting value for "" when sending cvars out
304                 if (thisname == name)
305                 {
306                         string s = func(this, strcat1(store.(field)));
307                         if (s != store.(field))
308                         {
309                                 strcpy(store.(field), s);
310                         }
311                 }
312 }
313 void GetCvars_handleFloat(entity this, entity store, string thisname, float f, .float field, string name)
314 {
315         if (f < 0)
316         {
317         }
318         else if (f > 0)
319         {
320                 if (thisname == name)
321                         store.(field) = stof(argv(f + 1));
322         }
323         else
324                 stuffcmd(this, strcat("cl_cmd sendcvar ", name, "\n"));
325 }
326 void GetCvars_handleFloatOnce(entity this, entity store, string thisname, float f, .float field, string name)
327 {
328         if (f < 0)
329         {
330         }
331         else if (f > 0)
332         {
333                 if (thisname == name)
334                 {
335                         if (!store.(field))
336                         {
337                                 store.(field) = stof(argv(f + 1));
338                                 if (!store.(field))
339                                         store.(field) = -1;
340                         }
341                 }
342         }
343         else
344         {
345                 if (!store.(field))
346                         stuffcmd(this, strcat("cl_cmd sendcvar ", name, "\n"));
347         }
348 }
349 string W_FixWeaponOrder_ForceComplete_AndBuildImpulseList(entity this, string wo)
350 {
351         string o = W_FixWeaponOrder_ForceComplete(wo);
352         strcpy(CS(this).weaponorder_byimpulse, W_FixWeaponOrder_BuildImpulseList(o));
353         return o;
354 }
355
356 /**
357  * @param f -1: cleanup, 0: request, 1: receive
358  */
359 void GetCvars(entity this, entity store, int f)
360 {
361         string s = string_null;
362
363         if (f == 0)
364                 LOG_INFO("Warning: requesting cvar values is deprecated. Client should send them automatically using REPLICATE.\n");
365
366         if (f > 0)
367                 s = strcat1(argv(f));
368
369         get_cvars_f = f;
370         get_cvars_s = s;
371         MUTATOR_CALLHOOK(GetCvars);
372
373         Notification_GetCvars(this);
374
375         ReplicateVars(this, store, s, f);
376
377         GetCvars_handleString_Fixup(this, store, s, f, cvar_cl_weaponpriority, "cl_weaponpriority", W_FixWeaponOrder_ForceComplete_AndBuildImpulseList);
378         GetCvars_handleString_Fixup(this, store, s, f, cvar_cl_weaponpriorities[0], "cl_weaponpriority0", W_FixWeaponOrder_AllowIncomplete);
379         GetCvars_handleString_Fixup(this, store, s, f, cvar_cl_weaponpriorities[1], "cl_weaponpriority1", W_FixWeaponOrder_AllowIncomplete);
380         GetCvars_handleString_Fixup(this, store, s, f, cvar_cl_weaponpriorities[2], "cl_weaponpriority2", W_FixWeaponOrder_AllowIncomplete);
381         GetCvars_handleString_Fixup(this, store, s, f, cvar_cl_weaponpriorities[3], "cl_weaponpriority3", W_FixWeaponOrder_AllowIncomplete);
382         GetCvars_handleString_Fixup(this, store, s, f, cvar_cl_weaponpriorities[4], "cl_weaponpriority4", W_FixWeaponOrder_AllowIncomplete);
383         GetCvars_handleString_Fixup(this, store, s, f, cvar_cl_weaponpriorities[5], "cl_weaponpriority5", W_FixWeaponOrder_AllowIncomplete);
384         GetCvars_handleString_Fixup(this, store, s, f, cvar_cl_weaponpriorities[6], "cl_weaponpriority6", W_FixWeaponOrder_AllowIncomplete);
385         GetCvars_handleString_Fixup(this, store, s, f, cvar_cl_weaponpriorities[7], "cl_weaponpriority7", W_FixWeaponOrder_AllowIncomplete);
386         GetCvars_handleString_Fixup(this, store, s, f, cvar_cl_weaponpriorities[8], "cl_weaponpriority8", W_FixWeaponOrder_AllowIncomplete);
387         GetCvars_handleString_Fixup(this, store, s, f, cvar_cl_weaponpriorities[9], "cl_weaponpriority9", W_FixWeaponOrder_AllowIncomplete);
388
389         GetCvars_handleFloat(this, store, s, f, cvar_cl_allow_uidtracking, "cl_allow_uidtracking");
390
391         // fixup of switchweapon (needed for LMS or when spectating is disabled, as PutClientInServer comes too early)
392         if (f > 0)
393         {
394                 if (s == "cl_weaponpriority")
395                 {
396                         for(int slot = 0; slot < MAX_WEAPONSLOTS; ++slot)
397                         {
398                                 .entity weaponentity = weaponentities[slot];
399                                 if (this.(weaponentity) && (this.(weaponentity).m_weapon != WEP_Null || slot == 0))
400                                         this.(weaponentity).m_switchweapon = w_getbestweapon(this, weaponentity);
401                         }
402                 }
403                 if (s == "cl_allow_uidtracking")
404                         PlayerStats_GameReport_AddPlayer(this);
405                 //if (s == "cl_gunalign")
406                         //W_ResetGunAlign(this, store.cvar_cl_gunalign);
407         }
408 }
409
410 // decolorizes and team colors the player name when needed
411 string playername(entity p, bool team_colorize)
412 {
413     string t;
414     if (team_colorize && teamplay && !intermission_running && IS_PLAYER(p))
415     {
416         t = Team_ColorCode(p.team);
417         return strcat(t, strdecolorize(p.netname));
418     }
419     else
420         return p.netname;
421 }
422
423 float want_weapon(entity weaponinfo, float allguns)
424 {
425         int d = 0;
426         bool allow_mutatorblocked = false;
427
428         if(!weaponinfo.m_id)
429                 return 0;
430
431         bool mutator_returnvalue = MUTATOR_CALLHOOK(WantWeapon, weaponinfo, d, allguns, allow_mutatorblocked);
432         d = M_ARGV(1, float);
433         allguns = M_ARGV(2, bool);
434         allow_mutatorblocked = M_ARGV(3, bool);
435
436         if(allguns)
437                 d = boolean((weaponinfo.spawnflags & WEP_FLAG_NORMAL) && !(weaponinfo.spawnflags & (WEP_FLAG_HIDDEN | WEP_FLAG_SPECIALATTACK)));
438         else if(!mutator_returnvalue)
439                 d = !(!weaponinfo.weaponstart);
440
441         if(!allow_mutatorblocked && (weaponinfo.spawnflags & WEP_FLAG_MUTATORBLOCKED)) // never default mutator blocked guns
442                 d = 0;
443
444         float t = weaponinfo.weaponstartoverride;
445
446         //LOG_INFOF("want_weapon: %s - d: %d t: %d\n", weaponinfo.netname, d, t);
447
448         // bit order in t:
449         // 1: want or not
450         // 2: is default?
451         // 4: is set by default?
452         if(t < 0)
453                 t = 4 | (3 * d);
454         else
455                 t |= (2 * d);
456
457         return t;
458 }
459
460 /// Weapons the player normally starts with outside weapon arena.
461 WepSet weapons_start()
462 {
463         WepSet ret = '0 0 0';
464         FOREACH(Weapons, it != WEP_Null, {
465                 int w = want_weapon(it, false);
466                 if (w & 1)
467                         ret |= it.m_wepset;
468         });
469         return ret;
470 }
471
472 WepSet weapons_all()
473 {
474         WepSet ret = '0 0 0';
475         FOREACH(Weapons, it != WEP_Null, {
476                 if (!(it.spawnflags & (WEP_FLAG_MUTATORBLOCKED | WEP_FLAG_SPECIALATTACK)))
477                         ret |= it.m_wepset;
478         });
479         return ret;
480 }
481
482 WepSet weapons_devall()
483 {
484         WepSet ret = '0 0 0';
485         FOREACH(Weapons, it != WEP_Null,
486         {
487                 ret |= it.m_wepset;
488         });
489         return ret;
490 }
491
492 WepSet weapons_most()
493 {
494         WepSet ret = '0 0 0';
495         FOREACH(Weapons, it != WEP_Null, {
496                 if ((it.spawnflags & WEP_FLAG_NORMAL) && !(it.spawnflags & (WEP_FLAG_MUTATORBLOCKED | WEP_FLAG_HIDDEN | WEP_FLAG_SPECIALATTACK)))
497                         ret |= it.m_wepset;
498         });
499         return ret;
500 }
501
502 void weaponarena_available_all_update(entity this)
503 {
504         if (weaponsInMapAll)
505         {
506                 start_weapons = warmup_start_weapons = g_weaponarena_weapons = weapons_start() | (weaponsInMapAll & weapons_all());
507         }
508         else
509         {
510                 // if no weapons are available on the map, just fall back to all weapons arena
511                 start_weapons = warmup_start_weapons = g_weaponarena_weapons = weapons_all();
512         }
513 }
514
515 void weaponarena_available_devall_update(entity this)
516 {
517         if (weaponsInMapAll)
518         {
519                 start_weapons = warmup_start_weapons = g_weaponarena_weapons = weapons_start() | weaponsInMapAll;
520         }
521         else
522         {
523                 // if no weapons are available on the map, just fall back to devall weapons arena
524                 start_weapons = warmup_start_weapons = g_weaponarena_weapons = weapons_devall();
525         }
526 }
527
528 void weaponarena_available_most_update(entity this)
529 {
530         if (weaponsInMapAll)
531         {
532                 start_weapons = warmup_start_weapons = g_weaponarena_weapons = weapons_start() | (weaponsInMapAll & weapons_most());
533         }
534         else
535         {
536                 // if no weapons are available on the map, just fall back to most weapons arena
537                 start_weapons = warmup_start_weapons = g_weaponarena_weapons = weapons_most();
538         }
539 }
540
541 void readplayerstartcvars()
542 {
543         // initialize starting values for players
544         start_weapons = '0 0 0';
545         start_weapons_default = '0 0 0';
546         start_weapons_defaultmask = '0 0 0';
547         start_items = 0;
548         start_ammo_shells = 0;
549         start_ammo_nails = 0;
550         start_ammo_rockets = 0;
551         start_ammo_cells = 0;
552         start_ammo_plasma = 0;
553         if (random_start_ammo == NULL)
554         {
555                 random_start_ammo = spawn();
556         }
557         start_health = cvar("g_balance_health_start");
558         start_armorvalue = cvar("g_balance_armor_start");
559
560         g_weaponarena = 0;
561         g_weaponarena_weapons = '0 0 0';
562
563         string s = cvar_string("g_weaponarena");
564
565         MUTATOR_CALLHOOK(SetWeaponArena, s);
566         s = M_ARGV(0, string);
567
568         if (s == "0" || s == "")
569         {
570                 // no arena
571         }
572         else if (s == "off")
573         {
574                 // forcibly turn off weaponarena
575         }
576         else if (s == "all" || s == "1")
577         {
578                 g_weaponarena = 1;
579                 g_weaponarena_list = "All Weapons";
580                 g_weaponarena_weapons = weapons_all();
581         }
582         else if (s == "devall")
583         {
584                 g_weaponarena = 1;
585                 g_weaponarena_list = "Dev All Weapons";
586                 g_weaponarena_weapons = weapons_devall();
587         }
588         else if (s == "most")
589         {
590                 g_weaponarena = 1;
591                 g_weaponarena_list = "Most Weapons";
592                 g_weaponarena_weapons = weapons_most();
593         }
594         else if (s == "all_available")
595         {
596                 g_weaponarena = 1;
597                 g_weaponarena_list = "All Available Weapons";
598
599                 // this needs to run after weaponsInMapAll is initialized
600                 InitializeEntity(NULL, weaponarena_available_all_update, INITPRIO_FINDTARGET);
601         }
602         else if (s == "devall_available")
603         {
604                 g_weaponarena = 1;
605                 g_weaponarena_list = "Dev All Available Weapons";
606
607                 // this needs to run after weaponsInMapAll is initialized
608                 InitializeEntity(NULL, weaponarena_available_devall_update, INITPRIO_FINDTARGET);
609         }
610         else if (s == "most_available")
611         {
612                 g_weaponarena = 1;
613                 g_weaponarena_list = "Most Available Weapons";
614
615                 // this needs to run after weaponsInMapAll is initialized
616                 InitializeEntity(NULL, weaponarena_available_most_update, INITPRIO_FINDTARGET);
617         }
618         else if (s == "none")
619         {
620                 g_weaponarena = 1;
621                 g_weaponarena_list = "No Weapons";
622         }
623         else
624         {
625                 g_weaponarena = 1;
626                 float t = tokenize_console(s);
627                 g_weaponarena_list = "";
628                 for (int j = 0; j < t; ++j)
629                 {
630                         s = argv(j);
631                         Weapon wep = Weapon_from_name(s);
632                         if(wep != WEP_Null)
633                         {
634                                 g_weaponarena_weapons |= (wep.m_wepset);
635                                 g_weaponarena_list = strcat(g_weaponarena_list, wep.m_name, " & ");
636                         }
637                 }
638                 g_weaponarena_list = strzone(substring(g_weaponarena_list, 0, strlen(g_weaponarena_list) - 3));
639         }
640
641         if (g_weaponarena)
642         {
643                 g_weapon_stay = 0; // incompatible
644                 start_weapons = g_weaponarena_weapons;
645                 start_items |= IT_UNLIMITED_AMMO | IT_UNLIMITED_SUPERWEAPONS;
646         }
647         else
648         {
649                 FOREACH(Weapons, it != WEP_Null, {
650                         int w = want_weapon(it, false);
651                         WepSet s = it.m_wepset;
652                         if(w & 1)
653                                 start_weapons |= s;
654                         if(w & 2)
655                                 start_weapons_default |= s;
656                         if(w & 4)
657                                 start_weapons_defaultmask |= s;
658                 });
659         }
660
661         if(cvar("g_balance_superweapons_time") < 0)
662                 start_items |= IT_UNLIMITED_SUPERWEAPONS;
663
664         if(!cvar("g_use_ammunition"))
665                 start_items |= IT_UNLIMITED_AMMO;
666
667         if(start_items & IT_UNLIMITED_AMMO)
668         {
669                 start_ammo_shells = 999;
670                 start_ammo_nails = 999;
671                 start_ammo_rockets = 999;
672                 start_ammo_cells = 999;
673                 start_ammo_plasma = 999;
674                 start_ammo_fuel = 999;
675         }
676         else
677         {
678                 start_ammo_shells = cvar("g_start_ammo_shells");
679                 start_ammo_nails = cvar("g_start_ammo_nails");
680                 start_ammo_rockets = cvar("g_start_ammo_rockets");
681                 start_ammo_cells = cvar("g_start_ammo_cells");
682                 start_ammo_plasma = cvar("g_start_ammo_plasma");
683                 start_ammo_fuel = cvar("g_start_ammo_fuel");
684                 random_start_weapons_count = cvar("g_random_start_weapons_count");
685                 SetResource(random_start_ammo, RES_SHELLS, cvar("g_random_start_shells"));
686                 SetResource(random_start_ammo, RES_BULLETS, cvar("g_random_start_bullets"));
687                 SetResource(random_start_ammo, RES_ROCKETS,cvar("g_random_start_rockets"));
688                 SetResource(random_start_ammo, RES_CELLS, cvar("g_random_start_cells"));
689                 SetResource(random_start_ammo, RES_PLASMA, cvar("g_random_start_plasma"));
690         }
691
692         warmup_start_ammo_shells = start_ammo_shells;
693         warmup_start_ammo_nails = start_ammo_nails;
694         warmup_start_ammo_rockets = start_ammo_rockets;
695         warmup_start_ammo_cells = start_ammo_cells;
696         warmup_start_ammo_plasma = start_ammo_plasma;
697         warmup_start_ammo_fuel = start_ammo_fuel;
698         warmup_start_health = start_health;
699         warmup_start_armorvalue = start_armorvalue;
700         warmup_start_weapons = start_weapons;
701         warmup_start_weapons_default = start_weapons_default;
702         warmup_start_weapons_defaultmask = start_weapons_defaultmask;
703
704         if (!g_weaponarena)
705         {
706                 warmup_start_ammo_shells = cvar("g_warmup_start_ammo_shells");
707                 warmup_start_ammo_nails = cvar("g_warmup_start_ammo_nails");
708                 warmup_start_ammo_rockets = cvar("g_warmup_start_ammo_rockets");
709                 warmup_start_ammo_cells = cvar("g_warmup_start_ammo_cells");
710                 warmup_start_ammo_plasma = cvar("g_warmup_start_ammo_plasma");
711                 warmup_start_ammo_fuel = cvar("g_warmup_start_ammo_fuel");
712                 warmup_start_health = cvar("g_warmup_start_health");
713                 warmup_start_armorvalue = cvar("g_warmup_start_armor");
714                 warmup_start_weapons = '0 0 0';
715                 warmup_start_weapons_default = '0 0 0';
716                 warmup_start_weapons_defaultmask = '0 0 0';
717                 FOREACH(Weapons, it != WEP_Null, {
718                         int w = want_weapon(it, autocvar_g_warmup_allguns);
719                         WepSet s = it.m_wepset;
720                         if(w & 1)
721                                 warmup_start_weapons |= s;
722                         if(w & 2)
723                                 warmup_start_weapons_default |= s;
724                         if(w & 4)
725                                 warmup_start_weapons_defaultmask |= s;
726                 });
727         }
728
729         if (autocvar_g_jetpack)
730                 start_items |= ITEM_Jetpack.m_itemid;
731
732         MUTATOR_CALLHOOK(SetStartItems);
733
734         if (start_items & ITEM_Jetpack.m_itemid)
735         {
736                 start_items |= ITEM_JetpackRegen.m_itemid;
737                 start_ammo_fuel = max(start_ammo_fuel, cvar("g_balance_fuel_rotstable"));
738                 warmup_start_ammo_fuel = max(warmup_start_ammo_fuel, cvar("g_balance_fuel_rotstable"));
739         }
740
741         start_ammo_shells = max(0, start_ammo_shells);
742         start_ammo_nails = max(0, start_ammo_nails);
743         start_ammo_rockets = max(0, start_ammo_rockets);
744         start_ammo_cells = max(0, start_ammo_cells);
745         start_ammo_plasma = max(0, start_ammo_plasma);
746         start_ammo_fuel = max(0, start_ammo_fuel);
747         SetResource(random_start_ammo, RES_SHELLS,
748                 max(0, GetResource(random_start_ammo, RES_SHELLS)));
749         SetResource(random_start_ammo, RES_BULLETS,
750                 max(0, GetResource(random_start_ammo, RES_BULLETS)));
751         SetResource(random_start_ammo, RES_ROCKETS,
752                 max(0, GetResource(random_start_ammo, RES_ROCKETS)));
753         SetResource(random_start_ammo, RES_CELLS,
754                 max(0, GetResource(random_start_ammo, RES_CELLS)));
755         SetResource(random_start_ammo, RES_PLASMA,
756                 max(0, GetResource(random_start_ammo, RES_PLASMA)));
757
758         warmup_start_ammo_shells = max(0, warmup_start_ammo_shells);
759         warmup_start_ammo_nails = max(0, warmup_start_ammo_nails);
760         warmup_start_ammo_rockets = max(0, warmup_start_ammo_rockets);
761         warmup_start_ammo_cells = max(0, warmup_start_ammo_cells);
762         warmup_start_ammo_plasma = max(0, warmup_start_ammo_plasma);
763         warmup_start_ammo_fuel = max(0, warmup_start_ammo_fuel);
764 }
765
766 void precache_playermodel(string m)
767 {
768         int globhandle, i, n;
769         string f;
770
771         // remove :<skinnumber> suffix
772         int j = strstrofs(m, ":", 0);
773         if(j >= 0)
774                 m = substring(m, 0, j);
775
776         if(substring(m, -9, 5) == "_lod1")
777                 return;
778         if(substring(m, -9, 5) == "_lod2")
779                 return;
780         precache_model(m);
781         f = strcat(substring(m, 0, -5), "_lod1", substring(m, -4, -1));
782         if(fexists(f))
783                 precache_model(f);
784         f = strcat(substring(m, 0, -5), "_lod2", substring(m, -4, -1));
785         if(fexists(f))
786                 precache_model(f);
787
788         globhandle = search_begin(strcat(m, "_*.sounds"), true, false);
789         if (globhandle < 0)
790                 return;
791         n = search_getsize(globhandle);
792         for (i = 0; i < n; ++i)
793         {
794                 //print(search_getfilename(globhandle, i), "\n");
795                 f = search_getfilename(globhandle, i);
796                 PrecachePlayerSounds(f);
797         }
798         search_end(globhandle);
799 }
800 void precache_all_playermodels(string pattern)
801 {
802         int globhandle = search_begin(pattern, true, false);
803         if (globhandle < 0) return;
804         int n = search_getsize(globhandle);
805         for (int i = 0; i < n; ++i)
806         {
807                 string s = search_getfilename(globhandle, i);
808                 precache_playermodel(s);
809         }
810         search_end(globhandle);
811 }
812
813 void precache_playermodels(string s)
814 {
815         FOREACH_WORD(s, true, { precache_playermodel(it); });
816 }
817
818 PRECACHE(PlayerModels)
819 {
820     // Precache all player models if desired
821     if (autocvar_sv_precacheplayermodels)
822     {
823         PrecachePlayerSounds("sound/player/default.sounds");
824         precache_all_playermodels("models/player/*.zym");
825         precache_all_playermodels("models/player/*.dpm");
826         precache_all_playermodels("models/player/*.md3");
827         precache_all_playermodels("models/player/*.psk");
828         precache_all_playermodels("models/player/*.iqm");
829     }
830
831     if (autocvar_sv_defaultcharacter)
832     {
833                 precache_playermodels(autocvar_sv_defaultplayermodel_red);
834                 precache_playermodels(autocvar_sv_defaultplayermodel_blue);
835                 precache_playermodels(autocvar_sv_defaultplayermodel_yellow);
836                 precache_playermodels(autocvar_sv_defaultplayermodel_pink);
837                 precache_playermodels(autocvar_sv_defaultplayermodel);
838     }
839 }
840
841 void InitializeEntity(entity e, void(entity this) func, int order)
842 {
843     entity prev, cur;
844
845     if (!e || e.initialize_entity)
846     {
847         // make a proxy initializer entity
848         entity e_old = e;
849         e = new(initialize_entity);
850         e.enemy = e_old;
851     }
852
853     e.initialize_entity = func;
854     e.initialize_entity_order = order;
855
856     cur = initialize_entity_first;
857     prev = NULL;
858     for (;;)
859     {
860         if (!cur || cur.initialize_entity_order > order)
861         {
862             // insert between prev and cur
863             if (prev)
864                 prev.initialize_entity_next = e;
865             else
866                 initialize_entity_first = e;
867             e.initialize_entity_next = cur;
868             return;
869         }
870         prev = cur;
871         cur = cur.initialize_entity_next;
872     }
873 }
874 void InitializeEntitiesRun()
875 {
876     entity startoflist = initialize_entity_first;
877     initialize_entity_first = NULL;
878     delete_fn = remove_except_protected;
879     for (entity e = startoflist; e; e = e.initialize_entity_next)
880     {
881                 e.remove_except_protected_forbidden = 1;
882     }
883     for (entity e = startoflist; e; )
884     {
885                 e.remove_except_protected_forbidden = 0;
886         e.initialize_entity_order = 0;
887         entity next = e.initialize_entity_next;
888         e.initialize_entity_next = NULL;
889         var void(entity this) func = e.initialize_entity;
890         e.initialize_entity = func_null;
891         if (e.classname == "initialize_entity")
892         {
893             entity wrappee = e.enemy;
894             builtin_remove(e);
895             e = wrappee;
896         }
897         //dprint("Delayed initialization: ", e.classname, "\n");
898         if (func)
899         {
900                 func(e);
901         }
902         else
903         {
904             eprint(e);
905             backtrace(strcat("Null function in: ", e.classname, "\n"));
906         }
907         e = next;
908     }
909     delete_fn = remove_unsafely;
910 }
911
912 void adaptor_think2use_hittype_splash(entity this) // for timed projectile detonation
913 {
914         if(!(IS_ONGROUND(this))) // if onground, we ARE touching something, but HITTYPE_SPLASH is to be networked if the damage causing projectile is not touching ANYTHING
915                 this.projectiledeathtype |= HITTYPE_SPLASH;
916         adaptor_think2use(this);
917 }
918
919 // deferred dropping
920 void DropToFloor_Handler(entity this)
921 {
922     WITHSELF(this, builtin_droptofloor());
923     this.dropped_origin = this.origin;
924 }
925
926 void droptofloor(entity this)
927 {
928     InitializeEntity(this, DropToFloor_Handler, INITPRIO_DROPTOFLOOR);
929 }
930
931
932
933 float trace_hits_box_a0, trace_hits_box_a1;
934
935 float trace_hits_box_1d(float end, float thmi, float thma)
936 {
937     if (end == 0)
938     {
939         // just check if x is in range
940         if (0 < thmi)
941             return false;
942         if (0 > thma)
943             return false;
944     }
945     else
946     {
947         // do the trace with respect to x
948         // 0 -> end has to stay in thmi -> thma
949         trace_hits_box_a0 = max(trace_hits_box_a0, min(thmi / end, thma / end));
950         trace_hits_box_a1 = min(trace_hits_box_a1, max(thmi / end, thma / end));
951         if (trace_hits_box_a0 > trace_hits_box_a1)
952             return false;
953     }
954     return true;
955 }
956
957 float trace_hits_box(vector start, vector end, vector thmi, vector thma)
958 {
959     end -= start;
960     thmi -= start;
961     thma -= start;
962     // now it is a trace from 0 to end
963
964     trace_hits_box_a0 = 0;
965     trace_hits_box_a1 = 1;
966
967     if (!trace_hits_box_1d(end.x, thmi.x, thma.x))
968         return false;
969     if (!trace_hits_box_1d(end.y, thmi.y, thma.y))
970         return false;
971     if (!trace_hits_box_1d(end.z, thmi.z, thma.z))
972         return false;
973
974     return true;
975 }
976
977 float tracebox_hits_box(vector start, vector mi, vector ma, vector end, vector thmi, vector thma)
978 {
979     return trace_hits_box(start, end, thmi - ma, thma - mi);
980 }
981
982 bool SUB_NoImpactCheck(entity this, entity toucher)
983 {
984         // zero hitcontents = this is not the real impact, but either the
985         // mirror-impact of something hitting the projectile instead of the
986         // projectile hitting the something, or a touchareagrid one. Neither of
987         // these stop the projectile from moving, so...
988         if(trace_dphitcontents == 0)
989         {
990                 LOG_TRACEF("A hit from a projectile happened with no hit contents! DEBUG THIS, this should never happen for projectiles! Projectile will self-destruct. (edict: %i, classname: %s, origin: %v)", this, this.classname, this.origin);
991                 checkclient(this); // TODO: .health is checked in the engine with this, possibly replace with a QC function?
992         }
993     if (trace_dphitq3surfaceflags & Q3SURFACEFLAG_NOIMPACT)
994         return true;
995     if (toucher == NULL && this.size != '0 0 0')
996     {
997         vector tic;
998         tic = this.velocity * sys_frametime;
999         tic = tic + normalize(tic) * vlen(this.maxs - this.mins);
1000         traceline(this.origin - tic, this.origin + tic, MOVE_NORMAL, this);
1001         if (trace_fraction >= 1)
1002         {
1003             LOG_TRACE("Odd... did not hit...?");
1004         }
1005         else if (trace_dphitq3surfaceflags & Q3SURFACEFLAG_NOIMPACT)
1006         {
1007             LOG_TRACE("Detected and prevented the sky-grapple bug.");
1008             return true;
1009         }
1010     }
1011
1012     return false;
1013 }
1014
1015 bool WarpZone_Projectile_Touch_ImpactFilter_Callback(entity this, entity toucher)
1016 {
1017         // owner check
1018         if(toucher && toucher == this.owner)
1019                 return true;
1020         if(SUB_NoImpactCheck(this, toucher))
1021         {
1022                 if(this.classname == "nade")
1023                         return false; // no checks here
1024                 else if(this.classname == "grapplinghook")
1025                         RemoveHook(this);
1026                 else
1027                         delete(this);
1028                 return true;
1029         }
1030         if(trace_ent && trace_ent.solid > SOLID_TRIGGER)
1031                 UpdateCSQCProjectile(this);
1032         return false;
1033 }
1034
1035 /** engine callback */
1036 void URI_Get_Callback(float id, float status, string data)
1037 {
1038         if(url_URI_Get_Callback(id, status, data))
1039         {
1040                 // handled
1041         }
1042         else if (id == URI_GET_DISCARD)
1043         {
1044                 // discard
1045         }
1046         else if (id >= URI_GET_CURL && id <= URI_GET_CURL_END)
1047         {
1048                 // sv_cmd curl
1049                 Curl_URI_Get_Callback(id, status, data);
1050         }
1051         else if (id >= URI_GET_IPBAN && id <= URI_GET_IPBAN_END)
1052         {
1053                 // online ban list
1054                 OnlineBanList_URI_Get_Callback(id, status, data);
1055         }
1056         else if (MUTATOR_CALLHOOK(URI_GetCallback, id, status, data))
1057         {
1058                 // handled by a mutator
1059         }
1060         else
1061         {
1062                 LOG_INFO("Received HTTP request data for an invalid id ", ftos(id), ".");
1063         }
1064 }
1065
1066 string uid2name(string myuid)
1067 {
1068         string s = db_get(ServerProgsDB, strcat("/uid2name/", myuid));
1069
1070         // FIXME remove this later after 0.6 release
1071         // convert old style broken records to correct style
1072         if(s == "")
1073         {
1074                 s = db_get(ServerProgsDB, strcat("uid2name", myuid));
1075                 if(s != "")
1076                 {
1077                         db_put(ServerProgsDB, strcat("/uid2name/", myuid), s);
1078                         db_remove(ServerProgsDB, strcat("uid2name", myuid));
1079                 }
1080         }
1081
1082         if(s == "")
1083                 s = "^1Unregistered Player";
1084         return s;
1085 }
1086
1087 bool MoveToRandomLocationWithinBounds(entity e, vector boundmin, vector boundmax, float goodcontents, float badcontents, float badsurfaceflags, int attempts, float maxaboveground, float minviewdistance)
1088 {
1089     float m = e.dphitcontentsmask;
1090     e.dphitcontentsmask = goodcontents | badcontents;
1091
1092     vector org = boundmin;
1093     vector delta = boundmax - boundmin;
1094
1095     vector start, end;
1096     start = end = org;
1097     int j; // used after the loop
1098     for(j = 0; j < attempts; ++j)
1099     {
1100         start.x = org.x + random() * delta.x;
1101         start.y = org.y + random() * delta.y;
1102         start.z = org.z + random() * delta.z;
1103
1104         // rule 1: start inside world bounds, and outside
1105         // solid, and don't start from somewhere where you can
1106         // fall down to evil
1107         tracebox(start, e.mins, e.maxs, start - '0 0 1' * delta.z, MOVE_NORMAL, e);
1108         if (trace_fraction >= 1)
1109             continue;
1110         if (trace_startsolid)
1111             continue;
1112         if (trace_dphitcontents & badcontents)
1113             continue;
1114         if (trace_dphitq3surfaceflags & badsurfaceflags)
1115             continue;
1116
1117         // rule 2: if we are too high, lower the point
1118         if (trace_fraction * delta.z > maxaboveground)
1119             start = trace_endpos + '0 0 1' * maxaboveground;
1120         vector enddown = trace_endpos;
1121
1122         // rule 3: make sure we aren't outside the map. This only works
1123         // for somewhat well formed maps. A good rule of thumb is that
1124         // the map should have a convex outside hull.
1125         // these can be traceLINES as we already verified the starting box
1126         vector mstart = start + 0.5 * (e.mins + e.maxs);
1127         traceline(mstart, mstart + '1 0 0' * delta.x, MOVE_NORMAL, e);
1128         if (trace_fraction >= 1 || trace_dphittexturename == "common/caulk")
1129             continue;
1130         traceline(mstart, mstart - '1 0 0' * delta.x, MOVE_NORMAL, e);
1131         if (trace_fraction >= 1 || trace_dphittexturename == "common/caulk")
1132             continue;
1133         traceline(mstart, mstart + '0 1 0' * delta.y, MOVE_NORMAL, e);
1134         if (trace_fraction >= 1 || trace_dphittexturename == "common/caulk")
1135             continue;
1136         traceline(mstart, mstart - '0 1 0' * delta.y, MOVE_NORMAL, e);
1137         if (trace_fraction >= 1 || trace_dphittexturename == "common/caulk")
1138             continue;
1139         traceline(mstart, mstart + '0 0 1' * delta.z, MOVE_NORMAL, e);
1140         if (trace_fraction >= 1 || trace_dphittexturename == "common/caulk")
1141             continue;
1142
1143                 // rule 4: we must "see" some spawnpoint or item
1144             entity sp = NULL;
1145             IL_EACH(g_spawnpoints, checkpvs(mstart, it),
1146             {
1147                 if((traceline(mstart, it.origin, MOVE_NORMAL, e), trace_fraction) >= 1)
1148                 {
1149                         sp = it;
1150                         break;
1151                 }
1152             });
1153                 if(!sp)
1154                 {
1155                         int items_checked = 0;
1156                         IL_EACH(g_items, checkpvs(mstart, it),
1157                         {
1158                                 if((traceline(mstart, it.origin + (it.mins + it.maxs) * 0.5, MOVE_NORMAL, e), trace_fraction) >= 1)
1159                                 {
1160                                         sp = it;
1161                                         break;
1162                                 }
1163
1164                                 ++items_checked;
1165                                 if(items_checked >= attempts)
1166                                         break; // sanity
1167                         });
1168
1169                         if(!sp)
1170                                 continue;
1171                 }
1172
1173         // find a random vector to "look at"
1174         end.x = org.x + random() * delta.x;
1175         end.y = org.y + random() * delta.y;
1176         end.z = org.z + random() * delta.z;
1177         end = start + normalize(end - start) * vlen(delta);
1178
1179         // rule 4: start TO end must not be too short
1180         tracebox(start, e.mins, e.maxs, end, MOVE_NORMAL, e);
1181         if(trace_startsolid)
1182             continue;
1183         if(trace_fraction < minviewdistance / vlen(delta))
1184             continue;
1185
1186         // rule 5: don't want to look at sky
1187         if(trace_dphitq3surfaceflags & Q3SURFACEFLAG_SKY)
1188             continue;
1189
1190         // rule 6: we must not end up in trigger_hurt
1191         if(tracebox_hits_trigger_hurt(start, e.mins, e.maxs, enddown))
1192             continue;
1193
1194         break;
1195     }
1196
1197     e.dphitcontentsmask = m;
1198
1199     if(j < attempts)
1200     {
1201         setorigin(e, start);
1202         e.angles = vectoangles(end - start);
1203         LOG_DEBUG("Needed ", ftos(j + 1), " attempts");
1204         return true;
1205     }
1206     return false;
1207 }
1208
1209 float MoveToRandomMapLocation(entity e, float goodcontents, float badcontents, float badsurfaceflags, float attempts, float maxaboveground, float minviewdistance)
1210 {
1211         return MoveToRandomLocationWithinBounds(e, world.mins, world.maxs, goodcontents, badcontents, badsurfaceflags, attempts, maxaboveground, minviewdistance);
1212 }
1213
1214 void write_recordmarker(entity pl, float tstart, float dt)
1215 {
1216     GameLogEcho(strcat(":recordset:", ftos(pl.playerid), ":", ftos(dt)));
1217
1218     // also write a marker into demo files for demotc-race-record-extractor to find
1219     stuffcmd(pl,
1220              strcat(
1221                  strcat("//", strconv(2, 0, 0, GetGametype()), " RECORD SET ", TIME_ENCODED_TOSTRING(TIME_ENCODE(dt))),
1222                  " ", ftos(tstart), " ", ftos(dt), "\n"));
1223 }
1224
1225 void attach_sameorigin(entity e, entity to, string tag)
1226 {
1227     vector org, t_forward, t_left, t_up, e_forward, e_up;
1228     float tagscale;
1229
1230     org = e.origin - gettaginfo(to, gettagindex(to, tag));
1231     tagscale = (vlen(v_forward) ** -2); // undo a scale on the tag
1232     t_forward = v_forward * tagscale;
1233     t_left = v_right * -tagscale;
1234     t_up = v_up * tagscale;
1235
1236     e.origin_x = org * t_forward;
1237     e.origin_y = org * t_left;
1238     e.origin_z = org * t_up;
1239
1240     // current forward and up directions
1241     if (substring(e.model, 0, 1) == "*") // bmodels have their own rules
1242                 e.angles = AnglesTransform_FromVAngles(e.angles);
1243         else
1244                 e.angles = AnglesTransform_FromAngles(e.angles);
1245     fixedmakevectors(e.angles);
1246
1247     // untransform forward, up!
1248     e_forward.x = v_forward * t_forward;
1249     e_forward.y = v_forward * t_left;
1250     e_forward.z = v_forward * t_up;
1251     e_up.x = v_up * t_forward;
1252     e_up.y = v_up * t_left;
1253     e_up.z = v_up * t_up;
1254
1255     e.angles = fixedvectoangles2(e_forward, e_up);
1256     if (substring(e.model, 0, 1) == "*") // bmodels have their own rules
1257                 e.angles = AnglesTransform_ToVAngles(e.angles);
1258         else
1259                 e.angles = AnglesTransform_ToAngles(e.angles);
1260
1261     setattachment(e, to, tag);
1262     setorigin(e, e.origin);
1263 }
1264
1265 void detach_sameorigin(entity e)
1266 {
1267     vector org;
1268     org = gettaginfo(e, 0);
1269     e.angles = fixedvectoangles2(v_forward, v_up);
1270     if (substring(e.model, 0, 1) == "*") // bmodels have their own rules
1271                 e.angles = AnglesTransform_ToVAngles(e.angles);
1272         else
1273                 e.angles = AnglesTransform_ToAngles(e.angles);
1274     setorigin(e, org);
1275     setattachment(e, NULL, "");
1276     setorigin(e, e.origin);
1277 }
1278
1279 void follow_sameorigin(entity e, entity to)
1280 {
1281     set_movetype(e, MOVETYPE_FOLLOW); // make the hole follow
1282     e.aiment = to; // make the hole follow bmodel
1283     e.punchangle = to.angles; // the original angles of bmodel
1284     e.view_ofs = e.origin - to.origin; // relative origin
1285     e.v_angle = e.angles - to.angles; // relative angles
1286 }
1287
1288 #if 0
1289 // TODO: unused, likely for a reason, possibly needs extensions (allow setting the new movetype as a parameter?)
1290 void unfollow_sameorigin(entity e)
1291 {
1292     set_movetype(e, MOVETYPE_NONE);
1293 }
1294 #endif
1295
1296 .string aiment_classname;
1297 .float aiment_deadflag;
1298 void SetMovetypeFollow(entity ent, entity e)
1299 {
1300         // FIXME this may not be warpzone aware
1301         set_movetype(ent, MOVETYPE_FOLLOW); // make the hole follow
1302         ent.solid = SOLID_NOT; // MOVETYPE_FOLLOW is always non-solid - this means this cannot be teleported by warpzones any more! Instead, we must notice when our owner gets teleported.
1303         ent.aiment = e; // make the hole follow bmodel
1304         ent.punchangle = e.angles; // the original angles of bmodel
1305         ent.view_ofs = ent.origin - e.origin; // relative origin
1306         ent.v_angle = ent.angles - e.angles; // relative angles
1307         ent.aiment_classname = strzone(e.classname);
1308         ent.aiment_deadflag = e.deadflag;
1309 }
1310 void UnsetMovetypeFollow(entity ent)
1311 {
1312         set_movetype(ent, MOVETYPE_FLY);
1313         PROJECTILE_MAKETRIGGER(ent);
1314         ent.aiment = NULL;
1315 }
1316 float LostMovetypeFollow(entity ent)
1317 {
1318 /*
1319         if(ent.move_movetype != MOVETYPE_FOLLOW)
1320                 if(ent.aiment)
1321                         error("???");
1322 */
1323         if(ent.aiment)
1324         {
1325                 if(ent.aiment.classname != ent.aiment_classname)
1326                         return 1;
1327                 if(ent.aiment.deadflag != ent.aiment_deadflag)
1328                         return 1;
1329         }
1330         return 0;
1331 }
1332
1333 string GetField_fullspawndata(entity e, string f, ...)
1334 /* Retrieves the value of a map entity field from fullspawndata
1335  * This bypasses field value changes made by the engine,
1336  * eg string-to-float and escape sequence substitution.
1337  *
1338  * Avoids the need to declare fields just to read them once :)
1339  *
1340  * Returns the last instance of the field to match DarkPlaces behaviour.
1341  * Path support: converts \ to / and tests the file if a third (bool, true) arg is passed.
1342  * Returns string_null if the entity does not have the field, or the file is not in the VFS.
1343  *
1344  * FIXME: entities with //comments are not supported.
1345  */
1346 {
1347         string v = string_null;
1348
1349         if (!e.fullspawndata)
1350         {
1351                 LOG_WARNF("^1EDICT %s (classname %s) has no fullspawndata, engine lacks support?", ftos(num_for_edict(e)), e.classname);
1352                 return v;
1353         }
1354
1355         if (strstrofs(e.fullspawndata, "//", 0) >= 0)
1356         {
1357                 // tokenize and tokenize_console return early if "//" is reached,
1358                 // which can leave an odd number of tokens and break key:value pairing.
1359                 LOG_WARNF("^1EDICT %s fullspawndata contains unsupported //comment^7%s", ftos(num_for_edict(e)), e.fullspawndata);
1360                 return v;
1361         }
1362
1363         //print(sprintf("%s(EDICT %s, FIELD %s)\n", __FUNC__, ftos(num_for_edict(e)), f));
1364         //print(strcat("FULLSPAWNDATA:", e.fullspawndata, "\n"));
1365
1366         // tokenize treats \ as an escape, but tokenize_console returns the required literal
1367         for (int t = tokenize_console(e.fullspawndata) - 3; t > 0; t -= 2)
1368         {
1369                 //print(sprintf("\tTOKEN %s:%s\t%s:%s\n", ftos(t), ftos(t + 1), argv(t), argv(t + 1)));
1370                 if (argv(t) == f)
1371                 {
1372                         v = argv(t + 1);
1373                         break;
1374                 }
1375         }
1376
1377         //print(strcat("RESULT: ", v, "\n\n"));
1378
1379         if (v && ...(0, bool) == true)
1380         {
1381                 v = strreplace("\\", "/", v);
1382                 if (whichpack(v) == "")
1383                         return string_null;
1384         }
1385
1386         return v;
1387 }