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