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