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