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