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