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