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