Predict warpzones
[xonotic/xonotic-data.pk3dir.git] / qcsrc / lib / warpzone / server.qc
1 #include "server.qh"
2
3 #include "common.qh"
4 #if defined(CSQC)
5 #elif defined(MENUQC)
6 #elif defined(SVQC)
7         #include "../../common/constants.qh"
8         #include "../../common/triggers/subs.qh"
9         #include "../../common/util.qh"
10         #include "../../server/command/common.qh"
11         #include "../../server/constants.qh"
12         #include "../../server/defs.qh"
13 #endif
14
15 #ifdef WARPZONELIB_KEEPDEBUG
16 #define WARPZONELIB_REMOVEHACK
17 #endif
18
19 // for think function
20 .vector warpzone_save_origin;
21 .vector warpzone_save_angles;
22 .vector warpzone_save_eorigin;
23 .vector warpzone_save_eangles;
24
25 // for all entities
26 .vector warpzone_oldorigin, warpzone_oldvelocity, warpzone_oldangles;
27 .float warpzone_teleport_time;
28 .float warpzone_teleport_finishtime;
29 .entity warpzone_teleport_zone;
30
31 void WarpZone_StoreProjectileData(entity e)
32 {
33 #ifdef SVQC
34         e.warpzone_oldorigin = e.origin;
35         e.warpzone_oldvelocity = e.velocity;
36         e.warpzone_oldangles = e.angles;
37 #elif defined(CSQC)
38         e.warpzone_oldorigin = e.move_origin;
39         e.warpzone_oldvelocity = e.move_velocity;
40         e.warpzone_oldangles = e.move_angles;
41 #endif
42 }
43
44 void WarpZone_TeleportPlayer(entity teleporter, entity player, vector to, vector to_angles, vector to_velocity)
45 {
46 #ifdef SVQC
47         setorigin (player, to); // NOTE: this also aborts the move, when this is called by touch
48         player.oldorigin = to; // for DP's unsticking
49         player.angles = to_angles;
50         player.fixangle = true;
51         player.velocity = to_velocity;
52 #elif defined(CSQC)
53         player.move_origin = to;
54         player.move_angles = to_angles;
55         player.move_velocity = to_velocity;
56 #endif
57
58         BITXOR_ASSIGN(player.effects, EF_TELEPORT_BIT);
59
60         if(IS_PLAYER(player))
61 #ifdef SVQC
62                 BITCLR_ASSIGN(player.flags, FL_ONGROUND);
63 #elif defined(CSQC)
64                 BITCLR_ASSIGN(player.move_flags, FL_ONGROUND);
65 #endif
66
67         WarpZone_PostTeleportPlayer_Callback(player);
68 }
69
70 #ifdef SVQC
71 bool WarpZone_Teleported_Send(entity to, int sf)
72 {SELFPARAM();
73         WriteHeader(MSG_ENTITY, ENT_CLIENT_WARPZONE_TELEPORTED);
74         WriteCoord(MSG_ENTITY, self.angles.x);
75         WriteCoord(MSG_ENTITY, self.angles.y);
76         WriteCoord(MSG_ENTITY, self.angles.z);
77         return true;
78 }
79 #endif
80
81 float WarpZone_Teleport(entity wz, entity player, float f0, float f1)
82 {
83         vector o0, a0, v0, o1, a1, v1, o10;
84
85 #ifdef SVQC
86         o0 = player.origin + player.view_ofs;
87         v0 = player.velocity;
88         a0 = player.angles;
89 #elif defined(CSQC)
90         o0 = player.move_origin + player.view_ofs;
91         v0 = player.move_velocity;
92         a0 = player.move_angles;
93 #endif
94
95         o10 = o1 = WarpZone_TransformOrigin(wz, o0);
96         v1 = WarpZone_TransformVelocity(wz, v0);
97         if (!IS_NOT_A_CLIENT(player))
98                 a1 = WarpZone_TransformVAngles(wz, player.v_angle);
99         else
100                 a1 = WarpZone_TransformAngles(wz, a0);
101
102         if(f0 != 0 || f1 != 0)
103         {
104                 // retry last move but behind the warpzone!
105                 // we must first go back as far as we can, then forward again, to not cause double touch events!
106
107                 tracebox(o1 - player.view_ofs + v1 * frametime * f1, player.mins, player.maxs, o1 - player.view_ofs + v1 * frametime * f0, MOVE_WORLDONLY, player);
108                 {
109                         entity own;
110                         own = player.owner;
111                         player.owner = world;
112                         tracebox(trace_endpos, player.mins, player.maxs, o1 - player.view_ofs + v1 * frametime * f1, MOVE_NORMAL, player); // this should get us through the warpzone
113                         player.owner = own;
114                 }
115                 o1 = trace_endpos + player.view_ofs;
116
117                 float d, dv, md;
118                 md = max(vlen(player.mins), vlen(player.maxs));
119                 d = WarpZone_TargetPlaneDist(wz, o1);
120                 dv = WarpZone_TargetPlaneDist(wz, v1);
121                 if(d < 0)
122                         o1 = o1 - v1 * (d / dv);
123         }
124
125         // put him out of solid
126         tracebox(o1 - player.view_ofs, player.mins, player.maxs, o1 - player.view_ofs, MOVE_NOMONSTERS, player);
127         if(trace_startsolid)
128         {
129                 setorigin(player, o1 - player.view_ofs);
130                 if(WarpZoneLib_MoveOutOfSolid(player))
131                 {
132                         o1 = player.origin + player.view_ofs;
133                         setorigin(player, o0 - player.view_ofs);
134                 }
135                 else
136                 {
137                         LOG_INFO("would have to put player in solid, won't do that\n");
138                         setorigin(player, o0 - player.view_ofs);
139                         return 0;
140                 }
141         }
142
143         // do the teleport
144         WarpZone_RefSys_Add(player, wz);
145         WarpZone_TeleportPlayer(wz, player, o1 - player.view_ofs, a1, v1);
146         WarpZone_StoreProjectileData(player);
147         player.warpzone_teleport_time = time;
148         player.warpzone_teleport_finishtime = time;
149         player.warpzone_teleport_zone = wz;
150
151 #ifdef SVQC
152         // prevent further teleports back
153         float dt = (o1 - o10) * v1 * (1 / (v1 * v1));
154         if(dt < PHYS_INPUT_FRAMETIME)
155                 player.warpzone_teleport_finishtime += PHYS_INPUT_FRAMETIME - dt;
156 #endif
157
158 #ifndef WARPZONE_USE_FIXANGLE
159         #ifdef SVQC
160         if(IS_VEHICLE(player) && player.owner)
161                 player = player.owner; // hax
162         if(IS_PLAYER(player))
163         {
164                 // instead of fixangle, send the transform to the client for smoother operation
165                 player.fixangle = false;
166
167                 entity ts = new(warpzone_teleported);
168                 setmodel(ts, MDL_Null);
169                 ts.SendEntity = WarpZone_Teleported_Send;
170                 ts.SendFlags = 0xFFFFFF;
171                 ts.drawonlytoclient = player;
172                 ts.think = SUB_Remove_self;
173                 ts.nextthink = time + 1;
174                 ts.owner = player;
175                 ts.enemy = wz;
176                 ts.effects = EF_NODEPTHTEST;
177                 ts.angles = wz.warpzone_transform;
178         }
179         #elif defined(CSQC)
180         setproperty(VF_CL_VIEWANGLES, WarpZone_TransformVAngles(wz, getpropertyvec(VF_CL_VIEWANGLES)));
181         if(checkextension("DP_CSQC_ROTATEMOVES"))
182                 CL_RotateMoves(wz.warpzone_transform);
183         #endif
184 #endif
185
186         return 1;
187 }
188
189 void WarpZone_Touch ()
190 {SELFPARAM();
191         if(other.classname == "trigger_warpzone")
192                 return;
193
194         if(time <= other.warpzone_teleport_finishtime) // already teleported this frame
195                 return;
196
197         // FIXME needs a better check to know what is safe to teleport and what not
198 #ifdef SVQC
199         if(other.movetype == MOVETYPE_NONE || other.movetype == MOVETYPE_FOLLOW || other.tag_entity)
200 #elif defined(CSQC)
201         if(other.move_movetype == MOVETYPE_NONE || other.move_movetype == MOVETYPE_FOLLOW || other.tag_networkentity)
202 #endif
203                 return;
204
205         if(WarpZoneLib_ExactTrigger_Touch())
206                 return;
207
208 #ifdef SVQC
209         if(WarpZone_PlaneDist(self, other.origin + other.view_ofs) >= 0) // wrong side of the trigger_warpzone (don't teleport yet)
210 #elif defined(CSQC)
211         if(WarpZone_PlaneDist(self, other.move_origin + other.view_ofs) >= 0) // wrong side of the trigger_warpzone (don't teleport yet)
212 #endif
213                 return;
214
215         float f;
216         // number of frames we need to go back:
217         //   dist = 16*sqrt(2) qu
218         //   dist ~ 24 qu
219         //   24 qu = v*t
220         //   24 qu = v*frametime*n
221         //       n = 24 qu/(v*frametime)
222         // for clients go only one frame though, may be too irritating otherwise
223         // but max 0.25 sec = 0.25/frametime frames
224         //       24/(0.25/frametime)
225         //       96*frametime
226         float d;
227         d = 24 + max(vlen(other.mins), vlen(other.maxs));
228         if(IS_NOT_A_CLIENT(other))
229         #ifdef SVQC
230                 f = -d / bound(frametime * d * 1, frametime * vlen(other.velocity), d);
231         #elif defined(CSQC)
232                 f = -d / bound(frametime * d * 1, frametime * vlen(other.move_velocity), d);
233         #endif
234         else
235                 f = -1;
236         if(WarpZone_Teleport(self, other, f, 0))
237         {
238 #ifdef SVQC
239                 string save1, save2;
240                 activator = other;
241
242                 save1 = self.target; self.target = string_null;
243                 save2 = self.target3; self.target3 = string_null;
244                 SUB_UseTargets();
245                 if (!self.target) self.target = save1;
246                 if (!self.target3) self.target3 = save2;
247
248                 setself(self.enemy);
249                 save1 = self.target; self.target = string_null;
250                 save2 = self.target2; self.target2 = string_null;
251                 SUB_UseTargets();
252                 if (!self.target) self.target = save1;
253                 if (!self.target2) self.target2 = save2;
254                 setself(this);
255 #endif
256         }
257         else
258         {
259                 LOG_TRACE("WARPZONE FAIL AHAHAHAHAH))\n");
260         }
261 }
262
263 #ifdef SVQC
264 bool WarpZone_Send(entity to, int sendflags)
265 {SELFPARAM();
266         WriteHeader(MSG_ENTITY, ENT_CLIENT_WARPZONE);
267
268         // we must send this flag for clientside to match properly too
269         int f = 0;
270         if(self.warpzone_isboxy)
271                 BITSET_ASSIGN(f, 1);
272         if(self.warpzone_fadestart)
273                 BITSET_ASSIGN(f, 2);
274         if(self.origin != '0 0 0')
275                 BITSET_ASSIGN(f, 4);
276         WriteByte(MSG_ENTITY, f);
277
278         // we need THESE to render the warpzone (and cull properly)...
279         if(f & 4)
280         {
281                 WriteCoord(MSG_ENTITY, self.origin.x);
282                 WriteCoord(MSG_ENTITY, self.origin.y);
283                 WriteCoord(MSG_ENTITY, self.origin.z);
284         }
285
286         WriteShort(MSG_ENTITY, self.modelindex);
287         WriteCoord(MSG_ENTITY, self.mins.x);
288         WriteCoord(MSG_ENTITY, self.mins.y);
289         WriteCoord(MSG_ENTITY, self.mins.z);
290         WriteCoord(MSG_ENTITY, self.maxs.x);
291         WriteCoord(MSG_ENTITY, self.maxs.y);
292         WriteCoord(MSG_ENTITY, self.maxs.z);
293         WriteByte(MSG_ENTITY, bound(1, self.scale * 16, 255));
294
295         // we need THESE to calculate the proper transform
296         WriteCoord(MSG_ENTITY, self.warpzone_origin.x);
297         WriteCoord(MSG_ENTITY, self.warpzone_origin.y);
298         WriteCoord(MSG_ENTITY, self.warpzone_origin.z);
299         WriteCoord(MSG_ENTITY, self.warpzone_angles.x);
300         WriteCoord(MSG_ENTITY, self.warpzone_angles.y);
301         WriteCoord(MSG_ENTITY, self.warpzone_angles.z);
302         WriteCoord(MSG_ENTITY, self.warpzone_targetorigin.x);
303         WriteCoord(MSG_ENTITY, self.warpzone_targetorigin.y);
304         WriteCoord(MSG_ENTITY, self.warpzone_targetorigin.z);
305         WriteCoord(MSG_ENTITY, self.warpzone_targetangles.x);
306         WriteCoord(MSG_ENTITY, self.warpzone_targetangles.y);
307         WriteCoord(MSG_ENTITY, self.warpzone_targetangles.z);
308
309         if(f & 2)
310         {
311                 WriteShort(MSG_ENTITY, self.warpzone_fadestart);
312                 WriteShort(MSG_ENTITY, self.warpzone_fadeend);
313         }
314
315         return true;
316 }
317
318 bool WarpZone_Camera_Send(entity to, int sendflags)
319 {SELFPARAM();
320         int f = 0;
321         WriteHeader(MSG_ENTITY, ENT_CLIENT_WARPZONE_CAMERA);
322
323         if(self.warpzone_fadestart)
324                 BITSET_ASSIGN(f, 2);
325         if(self.origin != '0 0 0')
326                 BITSET_ASSIGN(f, 4);
327         WriteByte(MSG_ENTITY, f);
328
329         // we need THESE to render the warpzone (and cull properly)...
330         if(f & 4)
331         {
332                 WriteCoord(MSG_ENTITY, self.origin.x);
333                 WriteCoord(MSG_ENTITY, self.origin.y);
334                 WriteCoord(MSG_ENTITY, self.origin.z);
335         }
336
337         WriteShort(MSG_ENTITY, self.modelindex);
338         WriteCoord(MSG_ENTITY, self.mins.x);
339         WriteCoord(MSG_ENTITY, self.mins.y);
340         WriteCoord(MSG_ENTITY, self.mins.z);
341         WriteCoord(MSG_ENTITY, self.maxs.x);
342         WriteCoord(MSG_ENTITY, self.maxs.y);
343         WriteCoord(MSG_ENTITY, self.maxs.z);
344         WriteByte(MSG_ENTITY, bound(1, self.scale * 16, 255));
345
346         // we need THESE to calculate the proper transform
347         WriteCoord(MSG_ENTITY, self.enemy.origin.x);
348         WriteCoord(MSG_ENTITY, self.enemy.origin.y);
349         WriteCoord(MSG_ENTITY, self.enemy.origin.z);
350         WriteCoord(MSG_ENTITY, self.enemy.angles.x);
351         WriteCoord(MSG_ENTITY, self.enemy.angles.y);
352         WriteCoord(MSG_ENTITY, self.enemy.angles.z);
353
354         if(f & 2)
355         {
356                 WriteShort(MSG_ENTITY, self.warpzone_fadestart);
357                 WriteShort(MSG_ENTITY, self.warpzone_fadeend);
358         }
359
360         return true;
361 }
362
363 #ifdef WARPZONELIB_KEEPDEBUG
364 float WarpZone_CheckProjectileImpact(entity player)
365 {SELFPARAM();
366         vector o0, v0;
367
368         .vector orgvec, velvec;
369 #ifdef SVQC
370         orgvec = origin;
371         velvec = velocity;
372 #elif defined(CSQC)
373         orgvec = move_origin;
374         velvec = move_velocity;
375 #endif
376
377         o0 = player.orgvec + player.view_ofs;
378         v0 = player.velvec;
379
380         // if we teleported shortly before, abort
381         if(time <= player.warpzone_teleport_finishtime + 0.1)
382                 return 0;
383
384         // if player hit a warpzone, abort
385         entity wz;
386         wz = WarpZone_Find(o0 + player.mins, o0 + player.maxs);
387         if(!wz)
388                 return 0;
389
390 #ifdef WARPZONELIB_REMOVEHACK
391         LOG_INFO("impactfilter found something - and it no longer gets handled correctly - please tell divVerent whether anything behaves broken now\n");
392 #else
393         LOG_INFO("impactfilter found something - and it even gets handled correctly - please tell divVerent that this code apparently gets triggered again\n");
394 #endif
395         LOG_INFO("Entity type: ", player.classname, "\n");
396         LOG_INFO("Origin: ", vtos(player.orgvec), "\n");
397         LOG_INFO("Velocity: ", vtos(player.velvec), "\n");
398
399 #ifdef WARPZONELIB_REMOVEHACK
400         return 0;
401 #else
402         // retry previous move
403 #ifdef SVQC
404         setorigin(player, player.warpzone_oldorigin);
405 #elif defined(CSQC)
406         player.move_origin = player.warpzone_oldorigin;
407 #endif
408         player.velvec = player.warpzone_oldvelocity;
409         if(WarpZone_Teleport(wz, player, 0, 1))
410         {
411                 entity oldself;
412                 string save1, save2;
413
414                 oldself = self;
415                 self = wz;
416                 other = player;
417                 activator = player;
418
419                 save1 = self.target; self.target = string_null;
420                 save2 = self.target3; self.target3 = string_null;
421                 SUB_UseTargets();
422                 if (!self.target) self.target = save1;
423                 if (!self.target3) self.target3 = save2;
424
425                 self = self.enemy;
426                 save1 = self.target; self.target = string_null;
427                 save2 = self.target2; self.target2 = string_null;
428                 SUB_UseTargets();
429                 if (!self.target) self.target = save1;
430                 if (!self.target2) self.target2 = save2;
431                 self = oldself;
432         }
433         else
434         {
435                 setorigin(player, o0 - player.view_ofs);
436                 player.velvec = v0;
437         }
438
439         return +1;
440 #endif
441 }
442 #endif
443 #endif
444
445 float WarpZone_Projectile_Touch()
446 {SELFPARAM();
447         if(other.classname == "trigger_warpzone")
448                 return true;
449
450         // no further impacts if we teleported this frame!
451         // this is because even if we did teleport, the engine still may raise
452         // touch events for the previous location
453         // engine now aborts moves on teleport, so this SHOULD not happen any more
454         // but if this is called from TouchAreaGrid of the projectile moving,
455         // then this won't do
456         if(time == self.warpzone_teleport_time)
457                 return true;
458
459 #ifdef SVQC
460 #ifdef WARPZONELIB_KEEPDEBUG
461         // this SEEMS to not happen at the moment, but if it did, it would be more reliable
462         {
463                 float save_dpstartcontents;
464                 float save_dphitcontents;
465                 float save_dphitq3surfaceflags;
466                 string save_dphittexturename;
467                 float save_allsolid;
468                 float save_startsolid;
469                 float save_fraction;
470                 vector save_endpos;
471                 vector save_plane_normal;
472                 float save_plane_dist;
473                 entity save_ent;
474                 float save_inopen;
475                 float save_inwater;
476                 save_dpstartcontents = trace_dpstartcontents;
477                 save_dphitcontents = trace_dphitcontents;
478                 save_dphitq3surfaceflags = trace_dphitq3surfaceflags;
479                 save_dphittexturename = trace_dphittexturename;
480                 save_allsolid = trace_allsolid;
481                 save_startsolid = trace_startsolid;
482                 save_fraction = trace_fraction;
483                 save_endpos = trace_endpos;
484                 save_plane_normal = trace_plane_normal;
485                 save_plane_dist = trace_plane_dist;
486                 save_ent = trace_ent;
487                 save_inopen = trace_inopen;
488                 save_inwater = trace_inwater;
489                 float f;
490                 if((f = WarpZone_CheckProjectileImpact(self)) != 0)
491                         return (f > 0);
492                 trace_dpstartcontents = save_dpstartcontents;
493                 trace_dphitcontents = save_dphitcontents;
494                 trace_dphitq3surfaceflags = save_dphitq3surfaceflags;
495                 trace_dphittexturename = save_dphittexturename;
496                 trace_allsolid = save_allsolid;
497                 trace_startsolid = save_startsolid;
498                 trace_fraction = save_fraction;
499                 trace_endpos = save_endpos;
500                 trace_plane_normal = save_plane_normal;
501                 trace_plane_dist = save_plane_dist;
502                 trace_ent = save_ent;
503                 trace_inopen = save_inopen;
504                 trace_inwater = save_inwater;
505         }
506 #endif
507
508         if(WarpZone_Projectile_Touch_ImpactFilter_Callback())
509                 return true;
510 #endif
511
512         return false;
513 }
514
515 #ifdef SVQC
516
517 void WarpZone_InitStep_FindOriginTarget()
518 {SELFPARAM();
519         if(self.killtarget != "")
520         {
521                 self.aiment = find(world, targetname, self.killtarget);
522                 if(self.aiment == world)
523                 {
524                         error("Warp zone with nonexisting killtarget");
525                         return;
526                 }
527                 self.killtarget = string_null;
528         }
529 }
530
531 void WarpZonePosition_InitStep_FindTarget()
532 {SELFPARAM();
533         if(self.target == "")
534         {
535                 error("Warp zone position with no target");
536                 return;
537         }
538         self.enemy = find(world, targetname, self.target);
539         if(self.enemy == world)
540         {
541                 error("Warp zone position with nonexisting target");
542                 return;
543         }
544         if(self.enemy.aiment)
545         {
546                 // already is positioned
547                 error("Warp zone position targeting already oriented warpzone");
548                 return;
549         }
550         self.enemy.aiment = self;
551 }
552
553 void WarpZoneCamera_Think()
554 {SELFPARAM();
555         if(self.warpzone_save_origin != self.origin
556         || self.warpzone_save_angles != self.angles
557         || self.warpzone_save_eorigin != self.enemy.origin
558         || self.warpzone_save_eangles != self.enemy.angles)
559         {
560                 WarpZone_Camera_SetUp(self, self.enemy.origin, self.enemy.angles);
561                 self.warpzone_save_origin = self.origin;
562                 self.warpzone_save_angles = self.angles;
563                 self.warpzone_save_eorigin = self.enemy.origin;
564                 self.warpzone_save_eangles = self.enemy.angles;
565         }
566         self.nextthink = time;
567 }
568
569 void WarpZoneCamera_InitStep_FindTarget()
570 {SELFPARAM();
571         entity e;
572         float i;
573         if(self.target == "")
574         {
575                 error("Camera with no target");
576                 return;
577         }
578         self.enemy = world;
579         for(e = world, i = 0; (e = find(e, targetname, self.target)); )
580                 if(random() * ++i < 1)
581                         self.enemy = e;
582         if(self.enemy == world)
583         {
584                 error("Camera with nonexisting target");
585                 return;
586         }
587         warpzone_cameras_exist = 1;
588         WarpZone_Camera_SetUp(self, self.enemy.origin, self.enemy.angles);
589         self.SendFlags = 0xFFFFFF;
590         if(self.spawnflags & 1)
591         {
592                 self.think = WarpZoneCamera_Think;
593                 self.nextthink = time;
594         }
595         else
596                 self.nextthink = 0;
597 }
598
599 void WarpZone_InitStep_UpdateTransform()
600 {SELFPARAM();
601         vector org, ang, norm, point;
602         float area;
603         vector tri, a, b, c, n;
604         float i_s, i_t, n_t;
605         string tex;
606
607         org = self.origin;
608         if(org == '0 0 0')
609                 org = 0.5 * (self.mins + self.maxs);
610
611         norm = point = '0 0 0';
612         area = 0;
613         for(i_s = 0; ; ++i_s)
614         {
615                 tex = getsurfacetexture(self, i_s);
616                 if (!tex)
617                         break; // this is beyond the last one
618                 if(tex == "textures/common/trigger" || tex == "trigger")
619                         continue;
620                 n_t = getsurfacenumtriangles(self, i_s);
621                 for(i_t = 0; i_t < n_t; ++i_t)
622                 {
623                         tri = getsurfacetriangle(self, i_s, i_t);
624                         a = getsurfacepoint(self, i_s, tri.x);
625                         b = getsurfacepoint(self, i_s, tri.y);
626                         c = getsurfacepoint(self, i_s, tri.z);
627                         n = cross(c - a, b - a);
628                         area = area + vlen(n);
629                         norm = norm + n;
630                         point = point + vlen(n) * (a + b + c);
631                 }
632         }
633         if(area > 0)
634         {
635                 norm = norm * (1 / area);
636                 point = point * (1 / (3 * area));
637                 if(vlen(norm) < 0.99)
638                 {
639                         LOG_INFO("trigger_warpzone near ", vtos(self.aiment.origin), " is nonplanar. BEWARE.\n");
640                         area = 0; // no autofixing in this case
641                 }
642                 norm = normalize(norm);
643         }
644
645         ang = '0 0 0';
646         if(self.aiment)
647         {
648                 org = self.aiment.origin;
649                 ang = self.aiment.angles;
650                 if(area > 0)
651                 {
652                         org = org - ((org - point) * norm) * norm; // project to plane
653                         makevectors(ang);
654                         if(norm * v_forward < 0)
655                         {
656                                 LOG_INFO("Position target of trigger_warpzone near ", vtos(self.aiment.origin), " points into trigger_warpzone. BEWARE.\n");
657                                 norm = -1 * norm;
658                         }
659                         ang = vectoangles2(norm, v_up); // keep rotation, but turn exactly against plane
660                         ang.x = -ang.x;
661                         if(norm * v_forward < 0.99)
662                                 LOG_INFO("trigger_warpzone near ", vtos(self.aiment.origin), " has been turned to match plane orientation (", vtos(self.aiment.angles), " -> ", vtos(ang), "\n");
663                         if(vlen(org - self.aiment.origin) > 0.5)
664                                 LOG_INFO("trigger_warpzone near ", vtos(self.aiment.origin), " has been moved to match the plane (", vtos(self.aiment.origin), " -> ", vtos(org), ").\n");
665                 }
666         }
667         else if(area > 0)
668         {
669                 org = point;
670                 ang = vectoangles(norm);
671                 ang.x = -ang.x;
672         }
673         else
674                 error("cannot infer origin/angles for this warpzone, please use a killtarget or a trigger_warpzone_position");
675
676         self.warpzone_origin = org;
677         self.warpzone_angles = ang;
678 }
679
680 void WarpZone_InitStep_ClearTarget()
681 {SELFPARAM();
682         if(self.enemy)
683                 self.enemy.enemy = world;
684         self.enemy = world;
685 }
686
687 entity warpzone_first; .entity warpzone_next;
688 void WarpZone_InitStep_FindTarget()
689 {SELFPARAM();
690         float i;
691         entity e, e2;
692
693         if(self.enemy)
694                 return;
695
696         // this way only one of the two ents needs to target
697         if(self.target != "")
698         {
699                 self.enemy = self; // so the if(!e.enemy) check also skips self, saves one IF
700
701                 e2 = world;
702                 for(e = world, i = 0; (e = find(e, targetname, self.target)); )
703                         if(!e.enemy)
704                                 if(e.classname == self.classname) // possibly non-warpzones may use the same targetname!
705                                         if(random() * ++i < 1)
706                                                 e2 = e;
707                 if(!e2)
708                 {
709                         self.enemy = world;
710                         error("Warpzone with non-existing target");
711                         return;
712                 }
713                 self.enemy = e2;
714                 e2.enemy = self;
715         }
716 }
717
718 void WarpZone_Think();
719 void WarpZone_InitStep_FinalizeTransform()
720 {SELFPARAM();
721         if(!self.enemy || self.enemy.enemy != self)
722         {
723                 error("Invalid warp zone detected. Killed.");
724                 return;
725         }
726
727         warpzone_warpzones_exist = 1;
728         WarpZone_SetUp(self, self.warpzone_origin, self.warpzone_angles, self.enemy.warpzone_origin, self.enemy.warpzone_angles);
729         self.touch = WarpZone_Touch;
730         self.SendFlags = 0xFFFFFF;
731         if(self.spawnflags & 1)
732         {
733                 self.think = WarpZone_Think;
734                 self.nextthink = time;
735         }
736         else
737                 self.nextthink = 0;
738 }
739
740 float warpzone_initialized;
741 //entity warpzone_first;
742 entity warpzone_position_first;
743 entity warpzone_camera_first;
744 .entity warpzone_next;
745 spawnfunc(misc_warpzone_position)
746 {
747         // "target", "angles", "origin"
748         self.warpzone_next = warpzone_position_first;
749         warpzone_position_first = self;
750 }
751 spawnfunc(trigger_warpzone_position)
752 {
753         spawnfunc_misc_warpzone_position(this);
754 }
755 spawnfunc(trigger_warpzone)
756 {
757         // warp zone entities must have:
758         // "killtarget" pointing to a target_position with a direction arrow
759         //              that points AWAY from the warp zone, and that is inside
760         //              the warp zone trigger
761         // "target"     pointing to an identical warp zone at another place in
762         //              the map, with another killtarget to designate its
763         //              orientation
764
765         if(!self.scale)
766                 self.scale = self.modelscale;
767         if(!self.scale)
768                 self.scale = 1;
769         string m;
770         m = self.model;
771         WarpZoneLib_ExactTrigger_Init();
772         if(m != "")
773         {
774                 precache_model(m);
775                 _setmodel(self, m); // no precision needed
776         }
777         setorigin(self, self.origin);
778         if(self.scale)
779                 setsize(self, self.mins * self.scale, self.maxs * self.scale);
780         else
781                 setsize(self, self.mins, self.maxs);
782         self.SendEntity = WarpZone_Send;
783         self.SendFlags = 0xFFFFFF;
784         BITSET_ASSIGN(self.effects, EF_NODEPTHTEST);
785         self.warpzone_next = warpzone_first;
786         warpzone_first = self;
787 }
788 spawnfunc(func_camera)
789 {
790         if(!self.scale)
791                 self.scale = self.modelscale;
792         if(!self.scale)
793                 self.scale = 1;
794         if(self.model != "")
795         {
796                 precache_model(self.model);
797                 _setmodel(self, self.model); // no precision needed
798         }
799         setorigin(self, self.origin);
800         if(self.scale)
801                 setsize(self, self.mins * self.scale, self.maxs * self.scale);
802         else
803                 setsize(self, self.mins, self.maxs);
804         if(!self.solid)
805                 self.solid = SOLID_BSP;
806         else if(self.solid < 0)
807                 self.solid = SOLID_NOT;
808         self.SendEntity = WarpZone_Camera_Send;
809         self.SendFlags = 0xFFFFFF;
810         self.warpzone_next = warpzone_camera_first;
811         warpzone_camera_first = self;
812 }
813 void WarpZones_Reconnect()
814 {SELFPARAM();
815         for(setself(warpzone_first); self; setself(self.warpzone_next))
816                 WarpZone_InitStep_ClearTarget();
817         for(setself(warpzone_first); self; setself(self.warpzone_next))
818                 WarpZone_InitStep_FindTarget();
819         for(setself(warpzone_camera_first); self; setself(self.warpzone_next))
820                 WarpZoneCamera_InitStep_FindTarget();
821         for(setself(warpzone_first); self; setself(self.warpzone_next))
822                 WarpZone_InitStep_FinalizeTransform();
823         setself(this);
824 }
825
826 void WarpZone_Think()
827 {SELFPARAM();
828         if(self.warpzone_save_origin != self.origin
829         || self.warpzone_save_angles != self.angles
830         || self.warpzone_save_eorigin != self.enemy.origin
831         || self.warpzone_save_eangles != self.enemy.angles)
832         {
833                 WarpZone_InitStep_UpdateTransform();
834                 setself(self.enemy);
835                 WarpZone_InitStep_UpdateTransform();
836                 setself(this);
837                 WarpZone_InitStep_FinalizeTransform();
838                 setself(self.enemy);
839                 WarpZone_InitStep_FinalizeTransform();
840                 setself(this);
841                 self.warpzone_save_origin = self.origin;
842                 self.warpzone_save_angles = self.angles;
843                 self.warpzone_save_eorigin = self.enemy.origin;
844                 self.warpzone_save_eangles = self.enemy.angles;
845         }
846         self.nextthink = time;
847 }
848
849 void WarpZone_StartFrame()
850 {
851         SELFPARAM();
852         if (!warpzone_initialized)
853         {
854                 warpzone_initialized = true;
855                 for (setself(warpzone_first); self; setself(self.warpzone_next))
856                         WarpZone_InitStep_FindOriginTarget();
857                 for (setself(warpzone_position_first); self; setself(self.warpzone_next))
858                         WarpZonePosition_InitStep_FindTarget();
859                 for (setself(warpzone_first); self; setself(self.warpzone_next))
860                         WarpZone_InitStep_UpdateTransform();
861                 setself(this);
862                 WarpZones_Reconnect();
863                 WarpZone_PostInitialize_Callback();
864         }
865
866         entity oldother = other;
867         for (entity e = world; (e = nextent(e)); )
868         {
869                 if (warpzone_warpzones_exist) WarpZone_StoreProjectileData(e);
870                 if (IS_REAL_CLIENT(e))
871                 {
872                         if (e.solid == SOLID_NOT) // not spectating?
873                         if (e.movetype == MOVETYPE_NOCLIP || e.movetype == MOVETYPE_FLY || e.movetype == MOVETYPE_FLY_WORLDONLY) // not spectating? (this is to catch observers)
874                         {
875                                 other = e; // player
876
877                                 // warpzones
878                                 if (warpzone_warpzones_exist) {
879                                         setself(WarpZone_Find(e.origin + e.mins, e.origin + e.maxs));
880                                         if (self)
881                                         if (!WarpZoneLib_ExactTrigger_Touch())
882                                         if (WarpZone_PlaneDist(self, e.origin + e.view_ofs) <= 0)
883                                                 WarpZone_Teleport(self, e, -1, 0); // NOT triggering targets by this!
884                                 }
885
886                                 // teleporters
887                                 setself(Teleport_Find(e.origin + e.mins, e.origin + e.maxs));
888                                 if (self)
889                                 if (!WarpZoneLib_ExactTrigger_Touch())
890                                         Simple_TeleportPlayer(self, other); // NOT triggering targets by this!
891                         }
892                 }
893                 else if (IS_NOT_A_CLIENT(e))
894                 {
895                         if (warpzone_warpzones_exist)
896                                 while ((e = nextent(e)))
897                                         WarpZone_StoreProjectileData(e);
898                         break;
899                 }
900         }
901         setself(this);
902         other = oldother;
903 }
904
905 .float warpzone_reconnecting;
906 bool visible_to_some_client(entity ent)
907 {
908         FOREACH_ENTITY(!IS_NOT_A_CLIENT(it), LAMBDA(
909                 if (IS_PLAYER(it) && IS_REAL_CLIENT(it) && checkpvs(it.origin + it.view_ofs, ent)) return true;
910         ));
911         return false;
912 }
913 void trigger_warpzone_reconnect_use()
914 {SELFPARAM();
915         entity e;
916         e = self;
917         // NOTE: this matches for target, not targetname, but of course
918         // targetname must be set too on the other entities
919         for(setself(warpzone_first); self; setself(self.warpzone_next))
920                 self.warpzone_reconnecting = ((e.target == "" || self.target == e.target) && !((e.spawnflags & 1) && (visible_to_some_client(self) || visible_to_some_client(self.enemy))));
921         for(setself(warpzone_camera_first); self; setself(self.warpzone_next))
922                 self.warpzone_reconnecting = ((e.target == "" || self.target == e.target) && !((e.spawnflags & 1) && visible_to_some_client(self)));
923         for(setself(warpzone_first); self; setself(self.warpzone_next))
924                 if(self.warpzone_reconnecting)
925                         WarpZone_InitStep_ClearTarget();
926         for(setself(warpzone_first); self; setself(self.warpzone_next))
927                 if(self.warpzone_reconnecting)
928                         WarpZone_InitStep_FindTarget();
929         for(setself(warpzone_camera_first); self; setself(self.warpzone_next))
930                 if(self.warpzone_reconnecting)
931                         WarpZoneCamera_InitStep_FindTarget();
932         for(setself(warpzone_first); self; setself(self.warpzone_next))
933                 if(self.warpzone_reconnecting || self.enemy.warpzone_reconnecting)
934                         WarpZone_InitStep_FinalizeTransform();
935         setself(e);
936 }
937
938 spawnfunc(trigger_warpzone_reconnect)
939 {
940         self.use = trigger_warpzone_reconnect_use;
941 }
942
943 spawnfunc(target_warpzone_reconnect)
944 {
945         spawnfunc_trigger_warpzone_reconnect(this); // both names make sense here :(
946 }
947
948 void WarpZone_PlayerPhysics_FixVAngle()
949 {SELFPARAM();
950 #ifndef WARPZONE_DONT_FIX_VANGLE
951         if(IS_REAL_CLIENT(self))
952         if(self.v_angle.z <= 360) // if not already adjusted
953         if(time - self.ping * 0.001 < self.warpzone_teleport_time)
954         {
955                 self.v_angle = WarpZone_TransformVAngles(self.warpzone_teleport_zone, self.v_angle);
956                 self.v_angle_z += 720; // mark as adjusted
957         }
958 #endif
959 }
960
961 #endif