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