780968375e4851bb661225060227104f0de362f2
[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                         MAKE_VECTORS_NEW(ang, forward, right, up);
606                         if(norm * forward < 0)
607                         {
608                                 LOG_INFO("Position target of trigger_warpzone near ", vtos(this.aiment.origin), " points into trigger_warpzone. BEWARE.");
609                                 norm = -1 * norm;
610                         }
611                         ang = vectoangles2(norm, up); // keep rotation, but turn exactly against plane
612                         ang.x = -ang.x;
613                         if(norm * forward < 0.99)
614                                 LOG_INFO("trigger_warpzone near ", vtos(this.aiment.origin), " has been turned to match plane orientation (", vtos(this.aiment.angles), " -> ", vtos(ang));
615                         if(vdist(org - this.aiment.origin, >, 0.5))
616                                 LOG_INFO("trigger_warpzone near ", vtos(this.aiment.origin), " has been moved to match the plane (", vtos(this.aiment.origin), " -> ", vtos(org), ").");
617                 }
618         }
619         else if(area > 0)
620         {
621                 org = point;
622                 ang = vectoangles(norm);
623                 ang.x = -ang.x;
624         }
625         else
626                 error("cannot infer origin/angles for this warpzone, please use a killtarget or a trigger_warpzone_position");
627
628         this.warpzone_origin = org;
629         this.warpzone_angles = ang;
630 }
631
632 void WarpZone_InitStep_ClearTarget(entity this)
633 {
634         if(this.enemy)
635                 this.enemy.enemy = NULL;
636         this.enemy = NULL;
637 }
638
639 void WarpZone_InitStep_FindTarget(entity this)
640 {
641         float i;
642         entity e, e2;
643
644         if(this.enemy)
645                 return;
646
647         // this way only one of the two ents needs to target
648         if(this.target != "")
649         {
650                 this.enemy = this; // so the if(!e.enemy) check also skips this, saves one IF
651
652                 e2 = NULL;
653                 for(e = NULL, i = 0; (e = find(e, targetname, this.target)); )
654                         if(!e.enemy)
655                                 if(e.classname == this.classname) // possibly non-warpzones may use the same targetname!
656                                         if(random() * ++i < 1)
657                                                 e2 = e;
658                 if(!e2)
659                 {
660                         this.enemy = NULL;
661                         error("Warpzone with non-existing target");
662                         return;
663                 }
664                 this.enemy = e2;
665                 e2.enemy = this;
666         }
667 }
668
669 void WarpZone_Think(entity this);
670 void WarpZone_InitStep_FinalizeTransform(entity this)
671 {
672         if(!this.enemy || this.enemy.enemy != this)
673         {
674                 error("Invalid warp zone detected. Killed.");
675                 return;
676         }
677
678         warpzone_warpzones_exist = 1;
679         WarpZone_SetUp(this, this.warpzone_origin, this.warpzone_angles, this.enemy.warpzone_origin, this.enemy.warpzone_angles);
680         settouch(this, WarpZone_Touch);
681         this.SendFlags = 0xFFFFFF;
682         if(this.spawnflags & 1)
683         {
684                 setthink(this, WarpZone_Think);
685                 this.nextthink = time;
686         }
687         else
688                 this.nextthink = 0;
689 }
690
691 float warpzone_initialized;
692 //entity warpzone_first;
693 entity warpzone_position_first;
694 entity warpzone_camera_first;
695 .entity warpzone_next;
696 spawnfunc(misc_warpzone_position)
697 {
698         // "target", "angles", "origin"
699         this.warpzone_next = warpzone_position_first;
700         warpzone_position_first = this;
701 }
702 spawnfunc(trigger_warpzone_position)
703 {
704         spawnfunc_misc_warpzone_position(this);
705 }
706 spawnfunc(trigger_warpzone)
707 {
708         // warp zone entities must have:
709         // "killtarget" pointing to a target_position with a direction arrow
710         //              that points AWAY from the warp zone, and that is inside
711         //              the warp zone trigger
712         // "target"     pointing to an identical warp zone at another place in
713         //              the map, with another killtarget to designate its
714         //              orientation
715
716         if(!this.scale)
717                 this.scale = this.modelscale;
718         if(!this.scale)
719                 this.scale = 1;
720         string m;
721         m = this.model;
722         WarpZoneLib_ExactTrigger_Init(this);
723         if(m != "")
724         {
725                 precache_model(m);
726                 _setmodel(this, m); // no precision needed
727         }
728         setorigin(this, this.origin);
729         if(this.scale)
730                 setsize(this, this.mins * this.scale, this.maxs * this.scale);
731         else
732                 setsize(this, this.mins, this.maxs);
733         setSendEntity(this, WarpZone_Send);
734         this.SendFlags = 0xFFFFFF;
735         BITSET_ASSIGN(this.effects, EF_NODEPTHTEST);
736         this.warpzone_next = warpzone_first;
737         warpzone_first = this;
738
739         IL_PUSH(g_warpzones, this);
740 }
741 spawnfunc(func_camera)
742 {
743         if(!this.scale)
744                 this.scale = this.modelscale;
745         if(!this.scale)
746                 this.scale = 1;
747         if(this.model != "")
748         {
749                 precache_model(this.model);
750                 _setmodel(this, this.model); // no precision needed
751         }
752         setorigin(this, this.origin);
753         if(this.scale)
754                 setsize(this, this.mins * this.scale, this.maxs * this.scale);
755         else
756                 setsize(this, this.mins, this.maxs);
757         if(!this.solid)
758                 this.solid = SOLID_BSP;
759         else if(this.solid < 0)
760                 this.solid = SOLID_NOT;
761         setSendEntity(this, WarpZone_Camera_Send);
762         this.SendFlags = 0xFFFFFF;
763         this.warpzone_next = warpzone_camera_first;
764         warpzone_camera_first = this;
765 }
766 void WarpZones_Reconnect()
767 {
768         for(entity e = warpzone_first; e; e = e.warpzone_next)
769                 WarpZone_InitStep_ClearTarget(e);
770         for(entity e = warpzone_first; e; e = e.warpzone_next)
771                 WarpZone_InitStep_FindTarget(e);
772         for(entity e = warpzone_camera_first; e; e = e.warpzone_next)
773                 WarpZoneCamera_InitStep_FindTarget(e);
774         for(entity e = warpzone_first; e; e = e.warpzone_next)
775                 WarpZone_InitStep_FinalizeTransform(e);
776 }
777
778 void WarpZone_Think(entity this)
779 {
780         if(this.warpzone_save_origin != this.origin
781         || this.warpzone_save_angles != this.angles
782         || this.warpzone_save_eorigin != this.enemy.origin
783         || this.warpzone_save_eangles != this.enemy.angles)
784         {
785                 WarpZone_InitStep_UpdateTransform(this);
786                 WarpZone_InitStep_UpdateTransform(this.enemy);
787                 WarpZone_InitStep_FinalizeTransform(this);
788                 WarpZone_InitStep_FinalizeTransform(this.enemy);
789                 this.warpzone_save_origin = this.origin;
790                 this.warpzone_save_angles = this.angles;
791                 this.warpzone_save_eorigin = this.enemy.origin;
792                 this.warpzone_save_eangles = this.enemy.angles;
793         }
794         this.nextthink = time;
795 }
796
797 void WarpZone_StartFrame()
798 {
799         if (!warpzone_initialized)
800         {
801                 warpzone_initialized = true;
802                 for(entity e = warpzone_first; e; e = e.warpzone_next)
803                         WarpZone_InitStep_FindOriginTarget(e);
804                 for(entity e = warpzone_position_first; e; e = e.warpzone_next)
805                         WarpZonePosition_InitStep_FindTarget(e);
806                 for(entity e = warpzone_first; e; e = e.warpzone_next)
807                         WarpZone_InitStep_UpdateTransform(e);
808                 WarpZones_Reconnect();
809                 WarpZone_PostInitialize_Callback();
810         }
811
812         if(warpzone_warpzones_exist)
813         {
814                 IL_EACH(g_projectiles, true,
815                 {
816                         WarpZone_StoreProjectileData(it);
817                 });
818         }
819
820
821         FOREACH_CLIENT(true,
822         {
823                 if(warpzone_warpzones_exist)
824                         WarpZone_StoreProjectileData(it); // TODO: not actually needed
825
826                 if(IS_OBSERVER(it) || it.solid == SOLID_NOT)
827                 if(IS_CLIENT(it)) // we don't care about it being a bot
828                 {
829                         // warpzones
830                         if (warpzone_warpzones_exist) {
831                                 entity e = WarpZone_Find(it.origin + it.mins, it.origin + it.maxs);
832                                 if (e)
833                                 if (!WarpZoneLib_ExactTrigger_Touch(e, it))
834                                 if (WarpZone_PlaneDist(e, it.origin + it.view_ofs) <= 0)
835                                         WarpZone_Teleport(e, it, -1, 0); // NOT triggering targets by this!
836                         }
837
838                         // teleporters
839                         if(it.teleportable)
840                         {
841                                 entity ent = Teleport_Find(it.origin + it.mins, it.origin + it.maxs);
842                                 if (ent)
843                                 if (!WarpZoneLib_ExactTrigger_Touch(ent, it))
844                                         Simple_TeleportPlayer(ent, it); // NOT triggering targets by this!
845                         }
846                 }
847         });
848 }
849
850 .float warpzone_reconnecting;
851 bool visible_to_some_client(entity ent)
852 {
853         FOREACH_CLIENT(IS_PLAYER(it) && IS_REAL_CLIENT(it) && checkpvs(it.origin + it.view_ofs, ent),
854         {
855                 return true;
856         });
857         return false;
858 }
859 void trigger_warpzone_reconnect_use(entity this, entity actor, entity trigger)
860 {
861         // NOTE: this matches for target, not targetname, but of course
862         // targetname must be set too on the other entities
863         for(entity e = warpzone_first; e; e = e.warpzone_next)
864                 e.warpzone_reconnecting = ((this.target == "" || e.target == this.target) && !((this.spawnflags & 1) && (visible_to_some_client(e) || visible_to_some_client(e.enemy))));
865         for(entity e = warpzone_camera_first; e; e = e.warpzone_next)
866                 e.warpzone_reconnecting = ((this.target == "" || e.target == this.target) && !((this.spawnflags & 1) && visible_to_some_client(e)));
867         for(entity e = warpzone_first; e; e = e.warpzone_next)
868                 if(e.warpzone_reconnecting)
869                         WarpZone_InitStep_ClearTarget(e);
870         for(entity e = warpzone_first; e; e = e.warpzone_next)
871                 if(e.warpzone_reconnecting)
872                         WarpZone_InitStep_FindTarget(e);
873         for(entity e = warpzone_camera_first; e; e = e.warpzone_next)
874                 if(e.warpzone_reconnecting)
875                         WarpZoneCamera_InitStep_FindTarget(e);
876         for(entity e = warpzone_first; e; e = e.warpzone_next)
877                 if(e.warpzone_reconnecting || e.enemy.warpzone_reconnecting)
878                         WarpZone_InitStep_FinalizeTransform(e);
879 }
880
881 spawnfunc(trigger_warpzone_reconnect)
882 {
883         this.use = trigger_warpzone_reconnect_use;
884 }
885
886 spawnfunc(target_warpzone_reconnect)
887 {
888         spawnfunc_trigger_warpzone_reconnect(this); // both names make sense here :(
889 }
890
891 void WarpZone_PlayerPhysics_FixVAngle(entity this)
892 {
893 #ifndef WARPZONE_DONT_FIX_VANGLE
894         if(IS_REAL_CLIENT(this))
895         if(this.v_angle.z <= 360) // if not already adjusted
896         if(time - CS(this).ping * 0.001 < this.warpzone_teleport_time)
897         {
898                 this.v_angle = WarpZone_TransformVAngles(this.warpzone_teleport_zone, this.v_angle);
899                 this.v_angle_z += 720; // mark as adjusted
900         }
901 #endif
902 }
903
904 #endif