]> de.git.xonotic.org Git - xonotic/xonotic-data.pk3dir.git/blob - qcsrc/warpzonelib/server.qc
help other qccs a bit
[xonotic/xonotic-data.pk3dir.git] / qcsrc / warpzonelib / server.qc
1 .vector warpzone_oldorigin, warpzone_oldvelocity, warpzone_oldangles;
2 .float warpzone_teleport_time;
3 .entity warpzone_teleport_zone;
4
5 void WarpZone_StoreProjectileData(entity e)
6 {
7         e.warpzone_oldorigin = e.origin;
8         e.warpzone_oldvelocity = e.velocity;
9         e.warpzone_oldangles = e.angles;
10 }
11
12 void WarpZone_TeleportPlayer(entity teleporter, entity player, vector to, vector to_angles, vector to_velocity)
13 {
14         vector from;
15
16         makevectors (to_angles);
17
18         from = player.origin;
19         setorigin (player, to);
20         player.oldorigin = to; // for DP's unsticking
21         player.angles = to_angles;
22         player.fixangle = TRUE;
23         player.velocity = to_velocity;
24
25         BITXOR_ASSIGN(player.effects, EF_TELEPORT_BIT);
26
27         if(player.classname == "player")
28                 BITCLR_ASSIGN(player.flags, FL_ONGROUND);
29
30         WarpZone_PostTeleportPlayer_Callback(player);
31 }
32
33 float WarpZone_Teleported_Send(entity to, float sf)
34 {
35         WriteByte(MSG_ENTITY, ENT_CLIENT_WARPZONE_TELEPORTED);
36         WriteCoord(MSG_ENTITY, self.angles_x);
37         WriteCoord(MSG_ENTITY, self.angles_y);
38         WriteCoord(MSG_ENTITY, self.angles_z);
39         return TRUE;
40 }
41
42 float WarpZone_Teleport(entity player)
43 {
44         vector o0, a0, v0, o1, a1, v1;
45
46         o0 = player.origin + player.view_ofs;
47         v0 = player.velocity;
48         a0 = player.angles;
49
50         if(WarpZone_PlaneDist(self, o0) >= 0) // wrong side of the trigger_warpzone
51                 return 2;
52         // no failure, we simply don't want to teleport yet; TODO in
53         // this situation we may want to create a temporary clone
54         // entity of the player to fix graphics glitch
55
56         o1 = WarpZone_TransformOrigin(self, o0);
57         v1 = WarpZone_TransformVelocity(self, v0);
58         if(clienttype(player) != CLIENTTYPE_NOTACLIENT)
59                 a1 = WarpZone_TransformVAngles(self, player.v_angle);
60         else
61                 a1 = WarpZone_TransformAngles(self, a0);
62
63         // put him inside solid
64         tracebox(o1 - player.view_ofs, player.mins, player.maxs, o1 - player.view_ofs, MOVE_NOMONSTERS, player);
65         if(trace_startsolid)
66         {
67                 vector mi, ma;
68                 mi = player.mins;
69                 ma = player.maxs;
70                 setsize(player, mi - player.view_ofs, ma - player.view_ofs);
71                 setorigin(player, o1);
72                 if(WarpZoneLib_MoveOutOfSolid(player))
73                 {
74                         o1 = player.origin;
75                         setsize(player, mi, ma);
76                         setorigin(player, o0);
77                 }
78                 else
79                 {
80                         print("would have to put player in solid, won't do that\n");
81                         setsize(player, mi, ma);
82                         setorigin(player, o0 - player.view_ofs);
83                         return 0; // cannot fix
84                 }
85         }
86
87         if(WarpZone_TargetPlaneDist(self, o1) <= 0)
88         {
89                 print("inconsistent warp zones or evil roundoff error\n");
90                 return 0;
91         }
92
93         //print(sprintf("warpzone: %f %f %f -> %f %f %f\n", o0_x, o0_y, o0_z, o1_x, o1_y, o1_z));
94
95         //o1 = trace_endpos;
96         WarpZone_RefSys_Add(player, self);
97         WarpZone_TeleportPlayer(self, player, o1 - player.view_ofs, a1, v1);
98         WarpZone_StoreProjectileData(player);
99         player.warpzone_teleport_time = time;
100         player.warpzone_teleport_zone = self;
101 #ifndef WARPZONE_USE_FIXANGLE
102         // instead of fixangle, send the transform to the client for smoother operation
103         player.fixangle = FALSE;
104
105         entity ts = spawn();
106         setmodel(ts, "null");
107         ts.SendEntity = WarpZone_Teleported_Send;
108         ts.SendFlags = 0xFFFFFF;
109         ts.drawonlytoclient = player;
110         ts.think = SUB_Remove;
111         ts.nextthink = time + 1;
112         ts.owner = player;
113         ts.enemy = self;
114         ts.effects = EF_NODEPTHTEST;
115         ts.classname = "warpzone_teleported";
116         ts.angles = self.warpzone_transform;
117 #endif
118
119         return 1;
120 }
121
122 void WarpZone_Touch (void)
123 {
124         entity oldself, e;
125
126         if(other.classname == "trigger_warpzone")
127                 return;
128
129         // FIXME needs a better check to know what is safe to teleport and what not
130         if(other.movetype == MOVETYPE_NONE)
131                 return;
132
133         if(WarpZoneLib_ExactTrigger_Touch())
134                 return;
135
136         e = self.enemy;
137         if(WarpZone_Teleport(other))
138         {
139                 string save1, save2;
140                 activator = other;
141
142                 save1 = self.target; self.target = string_null;
143                 save2 = self.target3; self.target3 = string_null;
144                 SUB_UseTargets();
145                 if not(self.target) self.target = save1;
146                 if not(self.target3) self.target3 = save2;
147
148                 oldself = self;
149                 self = self.enemy;
150                 save1 = self.target; self.target = string_null;
151                 save2 = self.target2; self.target2 = string_null;
152                 SUB_UseTargets();
153                 if not(self.target) self.target = save1;
154                 if not(self.target2) self.target2 = save2;
155                 self = oldself;
156         }
157         else
158         {
159                 dprint("WARPZONE FAIL AHAHAHAHAH))\n");
160         }
161 }
162
163 float WarpZone_Send(entity to, float sendflags)
164 {
165         float f;
166         WriteByte(MSG_ENTITY, ENT_CLIENT_WARPZONE);
167
168         // we must send this flag for clientside to match properly too
169         f = 0;
170         if(self.warpzone_isboxy)
171                 BITSET_ASSIGN(f, 1);
172         if(self.warpzone_fadestart)
173                 BITSET_ASSIGN(f, 2);
174         if(self.origin != '0 0 0')
175                 BITSET_ASSIGN(f, 4);
176         WriteByte(MSG_ENTITY, f);
177
178         // we need THESE to render the warpzone (and cull properly)...
179         if(f & 4)
180         {
181                 WriteCoord(MSG_ENTITY, self.origin_x);
182                 WriteCoord(MSG_ENTITY, self.origin_y);
183                 WriteCoord(MSG_ENTITY, self.origin_z);
184         }
185
186         WriteShort(MSG_ENTITY, self.modelindex);
187         WriteCoord(MSG_ENTITY, self.mins_x);
188         WriteCoord(MSG_ENTITY, self.mins_y);
189         WriteCoord(MSG_ENTITY, self.mins_z);
190         WriteCoord(MSG_ENTITY, self.maxs_x);
191         WriteCoord(MSG_ENTITY, self.maxs_y);
192         WriteCoord(MSG_ENTITY, self.maxs_z);
193         WriteByte(MSG_ENTITY, bound(1, self.scale * 16, 255));
194
195         // we need THESE to calculate the proper transform
196         WriteCoord(MSG_ENTITY, self.warpzone_origin_x);
197         WriteCoord(MSG_ENTITY, self.warpzone_origin_y);
198         WriteCoord(MSG_ENTITY, self.warpzone_origin_z);
199         WriteCoord(MSG_ENTITY, self.warpzone_angles_x);
200         WriteCoord(MSG_ENTITY, self.warpzone_angles_y);
201         WriteCoord(MSG_ENTITY, self.warpzone_angles_z);
202         WriteCoord(MSG_ENTITY, self.warpzone_targetorigin_x);
203         WriteCoord(MSG_ENTITY, self.warpzone_targetorigin_y);
204         WriteCoord(MSG_ENTITY, self.warpzone_targetorigin_z);
205         WriteCoord(MSG_ENTITY, self.warpzone_targetangles_x);
206         WriteCoord(MSG_ENTITY, self.warpzone_targetangles_y);
207         WriteCoord(MSG_ENTITY, self.warpzone_targetangles_z);
208
209         if(f & 2)
210         {
211                 WriteShort(MSG_ENTITY, self.warpzone_fadestart);
212                 WriteShort(MSG_ENTITY, self.warpzone_fadeend);
213         }
214
215         return TRUE;
216 }
217
218 float WarpZone_Camera_Send(entity to, float sendflags)
219 {
220         float f;
221         WriteByte(MSG_ENTITY, ENT_CLIENT_WARPZONE_CAMERA);
222
223         if(self.warpzone_fadestart)
224                 BITSET_ASSIGN(f, 2);
225         if(self.origin != '0 0 0')
226                 BITSET_ASSIGN(f, 4);
227         WriteByte(MSG_ENTITY, f);
228
229         // we need THESE to render the warpzone (and cull properly)...
230         if(f & 4)
231         {
232                 WriteCoord(MSG_ENTITY, self.origin_x);
233                 WriteCoord(MSG_ENTITY, self.origin_y);
234                 WriteCoord(MSG_ENTITY, self.origin_z);
235         }
236
237         WriteShort(MSG_ENTITY, self.modelindex);
238         WriteCoord(MSG_ENTITY, self.mins_x);
239         WriteCoord(MSG_ENTITY, self.mins_y);
240         WriteCoord(MSG_ENTITY, self.mins_z);
241         WriteCoord(MSG_ENTITY, self.maxs_x);
242         WriteCoord(MSG_ENTITY, self.maxs_y);
243         WriteCoord(MSG_ENTITY, self.maxs_z);
244         WriteByte(MSG_ENTITY, bound(1, self.scale * 16, 255));
245
246         // we need THESE to calculate the proper transform
247         WriteCoord(MSG_ENTITY, self.enemy.origin_x);
248         WriteCoord(MSG_ENTITY, self.enemy.origin_y);
249         WriteCoord(MSG_ENTITY, self.enemy.origin_z);
250         WriteCoord(MSG_ENTITY, self.enemy.angles_x);
251         WriteCoord(MSG_ENTITY, self.enemy.angles_y);
252         WriteCoord(MSG_ENTITY, self.enemy.angles_z);
253
254         if(f & 2)
255         {
256                 WriteShort(MSG_ENTITY, self.warpzone_fadestart);
257                 WriteShort(MSG_ENTITY, self.warpzone_fadeend);
258         }
259
260         return TRUE;
261 }
262
263 float WarpZone_CheckProjectileImpact()
264 {
265         // if self hit a warpzone, abort
266         vector o0, v0, a0;
267         float mpd, pd, dpd;
268         entity wz;
269         wz = WarpZone_Find(self.origin + self.mins, self.origin + self.maxs);
270         if(!wz)
271                 return 0;
272         if(self.warpzone_teleport_time == time)
273         {
274                 // just ignore if we got teleported this frame already and now hit a wall and are in a warpzone again (this will cause a detonation)
275                 // print("2 warps 1 frame\n");
276                 return -1;
277         }
278         o0 = self.origin;
279         v0 = self.velocity;
280         a0 = self.angles;
281
282         // this approach transports the projectile at its full speed, but does
283         // not properly retain the projectile trail (but we can't retain it
284         // easily anyway without delaying the projectile by two frames, so who
285         // cares)
286         WarpZone_TraceBox_ThroughZone(self.warpzone_oldorigin, self.mins, self.maxs, self.warpzone_oldorigin + self.warpzone_oldvelocity * frametime, MOVE_NORMAL, self, wz, WarpZone_trace_callback_t_null); // this will get us through the warpzone
287         setorigin(self, trace_endpos);
288         self.angles = WarpZone_TransformAngles(WarpZone_trace_transform, self.angles);
289         self.velocity = WarpZone_TransformVelocity(WarpZone_trace_transform, self.warpzone_oldvelocity);
290         
291         // in case we are in our warp zone post-teleport, shift the projectile forward a bit
292         mpd = max(vlen(self.mins), vlen(self.maxs));
293         pd = WarpZone_TargetPlaneDist(wz, self.origin);
294         if(pd < mpd)
295         {
296                 dpd = normalize(self.velocity) * wz.warpzone_targetforward;
297                 setorigin(self, self.origin + normalize(self.velocity) * ((mpd - pd) / dpd));
298                 if(!WarpZoneLib_MoveOutOfSolid(self))
299                 {
300                         setorigin(self, o0);
301                         self.angles = a0;
302                         self.velocity = v0;
303                         return 0;
304                 }
305         }
306         WarpZone_RefSys_Add(self, wz);
307         WarpZone_StoreProjectileData(self);
308         self.warpzone_teleport_time = time;
309
310         return +1;
311 }
312 float WarpZone_Projectile_Touch()
313 {
314         float f;
315         if(other.classname == "trigger_warpzone")
316                 return TRUE;
317         if(WarpZone_Projectile_Touch_ImpactFilter_Callback())
318                 return TRUE;
319         if((f = WarpZone_CheckProjectileImpact()) != 0)
320                 return (f > 0);
321         if(self.warpzone_teleport_time == time)
322         {
323                 // sequence: hit warpzone, get teleported, hit wall
324                 // print("2 hits 1 frame\n");
325                 setorigin(self, self.warpzone_oldorigin);
326                 self.velocity = self.warpzone_oldvelocity;
327                 self.angles = self.warpzone_oldangles;
328                 return TRUE;
329         }
330         return FALSE;
331 }
332
333 void WarpZone_InitStep_FindOriginTarget()
334 {
335         if(self.killtarget != "")
336         {
337                 self.aiment = find(world, targetname, self.killtarget);
338                 if(self.aiment == world)
339                 {
340                         error("Warp zone with nonexisting killtarget");
341                         return;
342                 }
343                 self.killtarget = string_null;
344         }
345 }
346
347 void WarpZonePosition_InitStep_FindTarget()
348 {
349         if(self.target == "")
350         {
351                 error("Warp zone position with no target");
352                 return;
353         }
354         self.enemy = find(world, targetname, self.target);
355         if(self.enemy == world)
356         {
357                 error("Warp zone position with nonexisting target");
358                 return;
359         }
360         if(self.enemy.aiment)
361         {
362                 // already is positioned
363                 error("Warp zone position targeting already oriented warpzone");
364                 return;
365         }
366         self.enemy.aiment = self;
367 }
368
369 void WarpZoneCamera_InitStep_FindTarget()
370 {
371         entity e;
372         float i;
373         if(self.target == "")
374         {
375                 error("Camera with no target");
376                 return;
377         }
378         self.enemy = world;
379         for(e = world, i = 0; (e = find(e, targetname, self.target)); )
380                 if(random() * ++i < 1)
381                         self.enemy = e;
382         if(self.enemy == world)
383         {
384                 error("Camera with nonexisting target");
385                 return;
386         }
387         warpzone_cameras_exist = 1;
388         WarpZone_Camera_SetUp(self, self.enemy.origin, self.enemy.angles);
389         self.SendFlags = 0xFFFFFF;
390 }
391
392 void WarpZone_InitStep_UpdateTransform()
393 {
394         vector org, ang, norm, point;
395         float area;
396         vector tri, a, b, c, p, q, n;
397         float i_s, i_t, n_t;
398         string tex;
399
400         org = self.origin;
401         if(org == '0 0 0')
402                 org = 0.5 * (self.mins + self.maxs);
403
404         norm = point = '0 0 0';
405         area = 0;
406         for(i_s = 0; ; ++i_s)
407         {
408                 tex = getsurfacetexture(self, i_s);
409                 if not(tex)
410                         break; // this is beyond the last one
411                 if(tex == "textures/common/trigger" || tex == "trigger")
412                         continue;
413                 n_t = getsurfacenumtriangles(self, i_s);
414                 for(i_t = 0; i_t < n_t; ++i_t)
415                 {
416                         tri = getsurfacetriangle(self, i_s, i_t);
417                         a = getsurfacepoint(self, i_s, tri_x);
418                         b = getsurfacepoint(self, i_s, tri_y);
419                         c = getsurfacepoint(self, i_s, tri_z);
420                         p = b - a;
421                         q = c - a;
422                         n =     '1 0 0' * (q_y * p_z - q_z * p_y)
423                         +       '0 1 0' * (q_z * p_x - q_x * p_z)
424                         +       '0 0 1' * (q_x * p_y - q_y * p_x);
425                         area = area + vlen(n);
426                         norm = norm + n;
427                         point = point + vlen(n) * (a + b + c);
428                 }
429         }
430         if(area > 0)
431         {
432                 norm = norm * (1 / area);
433                 point = point * (1 / (3 * area));
434                 if(vlen(norm) < 0.99)
435                 {
436                         print("trigger_warpzone near ", vtos(self.aiment.origin), " is nonplanar. BEWARE.\n");
437                         area = 0; // no autofixing in this case
438                 }
439                 norm = normalize(norm);
440         }
441
442         if(self.aiment)
443         {
444                 org = self.aiment.origin;
445                 ang = self.aiment.angles;
446                 if(area > 0)
447                 {
448                         org = org - ((org - point) * norm) * norm; // project to plane
449                         makevectors(ang);
450                         if(norm * v_forward < 0)
451                         {
452                                 print("Position target of trigger_warpzone near ", vtos(self.aiment.origin), " points into trigger_warpzone. BEWARE.\n");
453                                 norm = -1 * norm;
454                         }
455                         ang = vectoangles(norm, v_up); // keep rotation, but turn exactly against plane
456                         ang_x = -ang_x;
457                         if(norm * v_forward < 0.99)
458                                 print("trigger_warpzone near ", vtos(self.aiment.origin), " has been turned to match plane orientation (", vtos(self.aiment.angles), " -> ", vtos(ang), "\n");
459                         if(vlen(org - self.aiment.origin) > 0.5)
460                                 print("trigger_warpzone near ", vtos(self.aiment.origin), " has been moved to match the plane (", vtos(self.aiment.origin), " -> ", vtos(org), ").\n");
461                 }
462         }
463         else if(area > 0)
464         {
465                 org = point;
466                 ang = vectoangles(norm);
467                 ang_x = -ang_x;
468         }
469         else
470                 error("cannot infer origin/angles for this warpzone, please use a killtarget or a trigger_warpzone_position");
471
472         self.warpzone_origin = org;
473         self.warpzone_angles = ang;
474 }
475
476 void WarpZone_InitStep_ClearTarget()
477 {
478         if(self.enemy)
479                 self.enemy.enemy = world;
480         self.enemy = world;
481 }
482
483 entity warpzone_first; .entity warpzone_next;
484 void WarpZone_InitStep_FindTarget()
485 {
486         float i;
487         entity e, e2;
488
489         if(self.enemy)
490                 return;
491
492         // this way only one of the two ents needs to target
493         if(self.target != "")
494         {
495                 self.enemy = self; // so the if(!e.enemy) check also skips self, saves one IF
496
497                 e2 = world;
498                 for(e = world, i = 0; (e = find(e, targetname, self.target)); )
499                         if(!e.enemy)
500                                 if(e.classname == self.classname) // possibly non-warpzones may use the same targetname!
501                                         if(random() * ++i < 1)
502                                                 e2 = e;
503                 if(!e2)
504                 {
505                         self.enemy = world;
506                         error("Warpzone with non-existing target");
507                         return;
508                 }
509                 self.enemy = e2;
510                 e2.enemy = self;
511         }
512 }
513
514 void WarpZone_InitStep_FinalizeTransform()
515 {
516         if(!self.enemy || self.enemy.enemy != self)
517         {
518                 error("Invalid warp zone detected. Killed.");
519                 return;
520         }
521
522         warpzone_warpzones_exist = 1;
523         WarpZone_SetUp(self, self.warpzone_origin, self.warpzone_angles, self.enemy.warpzone_origin, self.enemy.warpzone_angles);
524         self.touch = WarpZone_Touch;
525         self.SendFlags = 0xFFFFFF;
526 }
527
528 float warpzone_initialized;
529 entity warpzone_first;
530 entity warpzone_position_first;
531 entity warpzone_camera_first;
532 .entity warpzone_next;
533 void spawnfunc_misc_warpzone_position(void)
534 {
535         // "target", "angles", "origin"
536         self.warpzone_next = warpzone_position_first;
537         warpzone_position_first = self;
538 }
539 void spawnfunc_trigger_warpzone_position(void)
540 {
541         spawnfunc_misc_warpzone_position();
542 }
543 void spawnfunc_trigger_warpzone(void)
544 {
545         // warp zone entities must have:
546         // "killtarget" pointing to a target_position with a direction arrow
547         //              that points AWAY from the warp zone, and that is inside
548         //              the warp zone trigger
549         // "target"     pointing to an identical warp zone at another place in
550         //              the map, with another killtarget to designate its
551         //              orientation
552
553         if(!self.scale)
554                 self.scale = self.modelscale;
555         if(!self.scale)
556                 self.scale = 1;
557         string m;
558         m = self.model;
559         WarpZoneLib_ExactTrigger_Init();
560         if(m != "")
561         {
562                 precache_model(m);
563                 setmodel(self, m); // no precision needed
564         }
565         setorigin(self, self.origin);
566         if(self.scale)
567                 setsize(self, self.mins * self.scale, self.maxs * self.scale);
568         else
569                 setsize(self, self.mins, self.maxs);
570         self.SendEntity = WarpZone_Send;
571         self.SendFlags = 0xFFFFFF;
572         BITSET_ASSIGN(self.effects, EF_NODEPTHTEST);
573         self.warpzone_next = warpzone_first;
574         warpzone_first = self;
575 }
576 void spawnfunc_func_camera(void)
577 {
578         if(!self.scale)
579                 self.scale = self.modelscale;
580         if(!self.scale)
581                 self.scale = 1;
582         if(self.model != "")
583         {
584                 precache_model(self.model);
585                 setmodel(self, self.model); // no precision needed
586         }
587         setorigin(self, self.origin);
588         if(self.scale)
589                 setsize(self, self.mins * self.scale, self.maxs * self.scale);
590         else
591                 setsize(self, self.mins, self.maxs);
592         if(!self.solid)
593                 self.solid = SOLID_BSP;
594         else if(self.solid < 0)
595                 self.solid = SOLID_NOT;
596         self.SendEntity = WarpZone_Camera_Send;
597         self.SendFlags = 0xFFFFFF;
598         self.warpzone_next = warpzone_camera_first;
599         warpzone_camera_first = self;
600 }
601 void WarpZones_Reconnect()
602 {
603         entity e;
604         e = self;
605         for(self = warpzone_first; self; self = self.warpzone_next)
606                 WarpZone_InitStep_ClearTarget();
607         for(self = warpzone_first; self; self = self.warpzone_next)
608                 WarpZone_InitStep_FindTarget();
609         for(self = warpzone_camera_first; self; self = self.warpzone_next)
610                 WarpZoneCamera_InitStep_FindTarget();
611         for(self = warpzone_first; self; self = self.warpzone_next)
612                 WarpZone_InitStep_FinalizeTransform();
613         self = e;
614 }
615
616 void WarpZone_StartFrame()
617 {
618         entity e;
619         if(warpzone_initialized == 0)
620         {
621                 warpzone_initialized = 1;
622                 e = self;
623                 for(self = warpzone_first; self; self = self.warpzone_next)
624                         WarpZone_InitStep_FindOriginTarget();
625                 for(self = warpzone_position_first; self; self = self.warpzone_next)
626                         WarpZonePosition_InitStep_FindTarget();
627                 for(self = warpzone_first; self; self = self.warpzone_next)
628                         WarpZone_InitStep_UpdateTransform();
629                 self = e;
630                 WarpZones_Reconnect();
631         }
632
633         if(warpzone_warpzones_exist)
634         {
635                 entity oldself, oldother;
636                 oldself = self;
637                 oldother = other;
638                 for(e = world; (e = nextent(e)); )
639                 {
640                         WarpZone_StoreProjectileData(e);
641                         float f;
642                         f = clienttype(e);
643                         if(f == CLIENTTYPE_REAL)
644                         {
645                                 if(e.solid != SOLID_NOT) // not spectating?
646                                         continue;
647                                 if(e.movetype != MOVETYPE_NOCLIP && e.movetype != MOVETYPE_FLY) // not spectating? (this is to catch observers)
648                                         continue;
649                                 self = WarpZone_Find(e.origin + e.mins, e.origin + e.maxs);
650                                 if(!self)
651                                         continue;
652                                 other = e;
653                                 if(WarpZoneLib_ExactTrigger_Touch())
654                                         continue;
655                                 WarpZone_Teleport(e); // NOT triggering targets by this!
656                         }
657                         if(f == CLIENTTYPE_NOTACLIENT)
658                         {
659                                 for(; (e = nextent(e)); )
660                                         WarpZone_StoreProjectileData(e);
661                                 break;
662                         }
663                 }
664                 self = oldself;
665                 other = oldother;
666         }
667 }
668
669 .float warpzone_reconnecting;
670 float visible_to_some_client(entity ent)
671 {
672         entity e;
673         for(e = nextent(world); clienttype(e) != CLIENTTYPE_NOTACLIENT; e = nextent(e))
674                 if(e.classname == "player" && clienttype(e) == CLIENTTYPE_REAL)
675                         if(checkpvs(e.origin + e.view_ofs, ent))
676                                 return 1;
677         return 0;
678 }
679 void trigger_warpzone_reconnect_use()
680 {
681         entity e;
682         e = self;
683         // NOTE: this matches for target, not targetname, but of course
684         // targetname must be set too on the other entities
685         for(self = warpzone_first; self; self = self.warpzone_next)
686                 self.warpzone_reconnecting = ((e.target == "" || self.target == e.target) && !((e.spawnflags & 1) && (visible_to_some_client(self) || visible_to_some_client(self.enemy))));
687         for(self = warpzone_camera_first; self; self = self.warpzone_next)
688                 self.warpzone_reconnecting = ((e.target == "" || self.target == e.target) && !((e.spawnflags & 1) && visible_to_some_client(self)));
689         for(self = warpzone_first; self; self = self.warpzone_next)
690                 if(self.warpzone_reconnecting)
691                         WarpZone_InitStep_ClearTarget();
692         for(self = warpzone_first; self; self = self.warpzone_next)
693                 if(self.warpzone_reconnecting)
694                         WarpZone_InitStep_FindTarget();
695         for(self = warpzone_camera_first; self; self = self.warpzone_next)
696                 if(self.warpzone_reconnecting)
697                         WarpZoneCamera_InitStep_FindTarget();
698         for(self = warpzone_first; self; self = self.warpzone_next)
699                 if(self.warpzone_reconnecting || self.enemy.warpzone_reconnecting)
700                         WarpZone_InitStep_FinalizeTransform();
701         self = e;
702 }
703
704 void spawnfunc_trigger_warpzone_reconnect()
705 {
706         self.use = trigger_warpzone_reconnect_use;
707 }
708
709 void spawnfunc_target_warpzone_reconnect()
710 {
711         spawnfunc_trigger_warpzone_reconnect(); // both names make sense here :(
712 }
713
714 void WarpZone_PlayerPhysics_FixVAngle(void)
715 {
716 #ifndef WARPZONE_DONT_FIX_VANGLE
717         if(clienttype(self) == CLIENTTYPE_REAL)
718         if(self.v_angle_z <= 360) // if not already adjusted
719         if(time - self.ping * 0.001 < self.warpzone_teleport_time)
720         {
721                 self.v_angle = WarpZone_TransformVAngles(self.warpzone_teleport_zone, self.v_angle);
722                 self.v_angle_z += 720; // mark as adjusted
723         }
724 #endif
725 }