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