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