|
|
1.1 root 1: #include "g_local.h"
2: #include "m_player.h"
3:
4: void ClientUserinfoChanged (edict_t *ent, char *userinfo);
5:
6: void SP_misc_teleporter_dest (edict_t *ent);
7:
8: //
9: // Gross, ugly, disgustuing hack section
10: //
11:
12: // this function is an ugly as hell hack to fix some map flaws
13: //
14: // the coop spawn spots on some maps are SNAFU. There are coop spots
15: // with the wrong targetname as well as spots with no name at all
16: //
17: // we use carnal knowledge of the maps to fix the coop spot targetnames to match
18: // that of the nearest named single player spot
19:
20: static void SP_FixCoopSpots (edict_t *self)
21: {
22: edict_t *spot;
23: vec3_t d;
24:
25: spot = NULL;
26:
27: while(1)
28: {
29: spot = G_Find(spot, FOFS(classname), "info_player_start");
30: if (!spot)
31: return;
32: if (!spot->targetname)
33: continue;
34: VectorSubtract(self->s.origin, spot->s.origin, d);
35: if (VectorLength(d) < 384)
36: {
37: if ((!self->targetname) || Q_stricmp(self->targetname, spot->targetname) != 0)
38: {
39: // gi.dprintf("FixCoopSpots changed %s at %s targetname from %s to %s\n", self->classname, vtos(self->s.origin), self->targetname, spot->targetname);
40: self->targetname = spot->targetname;
41: }
42: return;
43: }
44: }
45: }
46:
47: // now if that one wasn't ugly enough for you then try this one on for size
48: // some maps don't have any coop spots at all, so we need to create them
49: // where they should have been
50:
51: static void SP_CreateCoopSpots (edict_t *self)
52: {
53: edict_t *spot;
54:
55: if(Q_stricmp(level.mapname, "security") == 0)
56: {
57: spot = G_Spawn();
58: spot->classname = "info_player_coop";
59: spot->s.origin[0] = 188 - 64;
60: spot->s.origin[1] = -164;
61: spot->s.origin[2] = 80;
62: spot->targetname = "jail3";
63: spot->s.angles[1] = 90;
64:
65: spot = G_Spawn();
66: spot->classname = "info_player_coop";
67: spot->s.origin[0] = 188 + 64;
68: spot->s.origin[1] = -164;
69: spot->s.origin[2] = 80;
70: spot->targetname = "jail3";
71: spot->s.angles[1] = 90;
72:
73: spot = G_Spawn();
74: spot->classname = "info_player_coop";
75: spot->s.origin[0] = 188 + 128;
76: spot->s.origin[1] = -164;
77: spot->s.origin[2] = 80;
78: spot->targetname = "jail3";
79: spot->s.angles[1] = 90;
80:
81: return;
82: }
83: }
84:
85:
86: /*QUAKED info_player_start (1 0 0) (-16 -16 -24) (16 16 32)
87: The normal starting point for a level.
88: */
89: void SP_info_player_start(edict_t *self)
90: {
91: if (!coop->value)
92: return;
93: if(Q_stricmp(level.mapname, "security") == 0)
94: {
95: // invoke one of our gross, ugly, disgusting hacks
96: self->think = SP_CreateCoopSpots;
97: self->nextthink = level.time + FRAMETIME;
98: }
99: }
100:
101: /*QUAKED info_player_deathmatch (1 0 1) (-16 -16 -24) (16 16 32)
102: potential spawning position for deathmatch games
103: */
104: void SP_info_player_deathmatch(edict_t *self)
105: {
106: if (!deathmatch->value)
107: {
108: G_FreeEdict (self);
109: return;
110: }
111: SP_misc_teleporter_dest (self);
112: }
113:
114: /*QUAKED info_player_coop (1 0 1) (-16 -16 -24) (16 16 32)
115: potential spawning position for coop games
116: */
117:
118: void SP_info_player_coop(edict_t *self)
119: {
120: if (!coop->value)
121: {
122: G_FreeEdict (self);
123: return;
124: }
125:
126: if((Q_stricmp(level.mapname, "jail2") == 0) ||
127: (Q_stricmp(level.mapname, "jail4") == 0) ||
128: (Q_stricmp(level.mapname, "mine1") == 0) ||
129: (Q_stricmp(level.mapname, "mine2") == 0) ||
130: (Q_stricmp(level.mapname, "mine3") == 0) ||
131: (Q_stricmp(level.mapname, "mine4") == 0) ||
132: (Q_stricmp(level.mapname, "lab") == 0) ||
133: (Q_stricmp(level.mapname, "boss1") == 0) ||
134: (Q_stricmp(level.mapname, "fact3") == 0) ||
135: (Q_stricmp(level.mapname, "biggun") == 0) ||
136: (Q_stricmp(level.mapname, "space") == 0) ||
137: (Q_stricmp(level.mapname, "command") == 0) ||
138: (Q_stricmp(level.mapname, "power2") == 0) ||
139: (Q_stricmp(level.mapname, "strike") == 0))
140: {
141: // invoke one of our gross, ugly, disgusting hacks
142: self->think = SP_FixCoopSpots;
143: self->nextthink = level.time + FRAMETIME;
144: }
145: }
146:
147:
148: /*QUAKED info_player_intermission (1 0 1) (-16 -16 -24) (16 16 32)
149: The deathmatch intermission point will be at one of these
150: Use 'angles' instead of 'angle', so you can set pitch or roll as well as yaw. 'pitch yaw roll'
151: */
152: void SP_info_player_intermission(void)
153: {
154: }
155:
156:
157: //=======================================================================
158:
159:
160: void player_pain (edict_t *self, edict_t *other, float kick, int damage)
161: {
162: // player pain is handled at the end of the frame in P_DamageFeedback
163: }
164:
165:
166: qboolean IsFemale (edict_t *ent)
167: {
168: char *info;
169:
170: if (!ent->client)
171: return false;
172:
173: info = Info_ValueForKey (ent->client->pers.userinfo, "gender");
174: if (info[0] == 'f' || info[0] == 'F')
175: return true;
176: return false;
177: }
178:
179: qboolean IsNeutral (edict_t *ent)
180: {
181: char *info;
182:
183: if (!ent->client)
184: return false;
185:
186: info = Info_ValueForKey (ent->client->pers.userinfo, "gender");
187: if (info[0] != 'f' && info[0] != 'F' && info[0] != 'm' && info[0] != 'M')
188: return true;
189: return false;
190: }
191:
192: void ClientObituary (edict_t *self, edict_t *inflictor, edict_t *attacker)
193: {
194: int mod;
195: char *message;
196: char *message2;
197: qboolean ff;
198:
199: if (coop->value && attacker->client)
200: meansOfDeath |= MOD_FRIENDLY_FIRE;
201:
202: if (deathmatch->value || coop->value)
203: {
204: ff = meansOfDeath & MOD_FRIENDLY_FIRE;
205: mod = meansOfDeath & ~MOD_FRIENDLY_FIRE;
206: message = NULL;
207: message2 = "";
208:
209: switch (mod)
210: {
211: case MOD_SUICIDE:
212: message = "suicides";
213: break;
214: case MOD_FALLING:
215: message = "cratered";
216: break;
217: case MOD_CRUSH:
218: message = "was squished";
219: break;
220: case MOD_WATER:
221: message = "sank like a rock";
222: break;
223: case MOD_SLIME:
224: message = "melted";
225: break;
226: case MOD_LAVA:
227: message = "does a back flip into the lava";
228: break;
229: case MOD_EXPLOSIVE:
230: case MOD_BARREL:
231: message = "blew up";
232: break;
233: case MOD_EXIT:
234: message = "found a way out";
235: break;
236: case MOD_TARGET_LASER:
237: message = "saw the light";
238: break;
239: case MOD_TARGET_BLASTER:
240: message = "got blasted";
241: break;
242: case MOD_BOMB:
243: case MOD_SPLASH:
244: case MOD_TRIGGER_HURT:
245: message = "was in the wrong place";
246: break;
247: }
248: if (attacker == self)
249: {
250: switch (mod)
251: {
252: case MOD_HELD_GRENADE:
253: message = "tried to put the pin back in";
254: break;
255: case MOD_HG_SPLASH:
256: case MOD_G_SPLASH:
257: if (IsNeutral(self))
258: message = "tripped on its own grenade";
259: else if (IsFemale(self))
260: message = "tripped on her own grenade";
261: else
262: message = "tripped on his own grenade";
263: break;
264: case MOD_R_SPLASH:
265: if (IsNeutral(self))
266: message = "blew itself up";
267: else if (IsFemale(self))
268: message = "blew herself up";
269: else
270: message = "blew himself up";
271: break;
272: case MOD_BFG_BLAST:
273: message = "should have used a smaller gun";
274: break;
275: default:
276: if (IsNeutral(self))
277: message = "killed itself";
278: else if (IsFemale(self))
279: message = "killed herself";
280: else
281: message = "killed himself";
282: break;
283: }
284: }
285: if (message)
286: {
287: gi.bprintf (PRINT_MEDIUM, "%s %s.\n", self->client->pers.netname, message);
288: if (deathmatch->value)
289: self->client->resp.score--;
290: self->enemy = NULL;
291: return;
292: }
293:
294: self->enemy = attacker;
295: if (attacker && attacker->client)
296: {
297: switch (mod)
298: {
299: case MOD_BLASTER:
300: message = "was blasted by";
301: break;
302: case MOD_SHOTGUN:
303: message = "was gunned down by";
304: break;
305: case MOD_SSHOTGUN:
306: message = "was blown away by";
307: message2 = "'s super shotgun";
308: break;
309: case MOD_MACHINEGUN:
310: message = "was machinegunned by";
311: break;
312: case MOD_CHAINGUN:
313: message = "was cut in half by";
314: message2 = "'s chaingun";
315: break;
316: case MOD_GRENADE:
317: message = "was popped by";
318: message2 = "'s grenade";
319: break;
320: case MOD_G_SPLASH:
321: message = "was shredded by";
322: message2 = "'s shrapnel";
323: break;
324: case MOD_ROCKET:
325: message = "ate";
326: message2 = "'s rocket";
327: break;
328: case MOD_R_SPLASH:
329: message = "almost dodged";
330: message2 = "'s rocket";
331: break;
332: case MOD_HYPERBLASTER:
333: message = "was melted by";
334: message2 = "'s hyperblaster";
335: break;
336: case MOD_RAILGUN:
337: message = "was railed by";
338: break;
339: case MOD_BFG_LASER:
340: message = "saw the pretty lights from";
341: message2 = "'s BFG";
342: break;
343: case MOD_BFG_BLAST:
344: message = "was disintegrated by";
345: message2 = "'s BFG blast";
346: break;
347: case MOD_BFG_EFFECT:
348: message = "couldn't hide from";
349: message2 = "'s BFG";
350: break;
351: case MOD_HANDGRENADE:
352: message = "caught";
353: message2 = "'s handgrenade";
354: break;
355: case MOD_HG_SPLASH:
356: message = "didn't see";
357: message2 = "'s handgrenade";
358: break;
359: case MOD_HELD_GRENADE:
360: message = "feels";
361: message2 = "'s pain";
362: break;
363: case MOD_TELEFRAG:
364: message = "tried to invade";
365: message2 = "'s personal space";
366: break;
367: }
368: if (message)
369: {
370: gi.bprintf (PRINT_MEDIUM,"%s %s %s%s\n", self->client->pers.netname, message, attacker->client->pers.netname, message2);
371: if (deathmatch->value)
372: {
373: if (ff)
374: attacker->client->resp.score--;
375: else
376: attacker->client->resp.score++;
377: }
378: return;
379: }
380: }
381: }
382:
383: gi.bprintf (PRINT_MEDIUM,"%s died.\n", self->client->pers.netname);
384: if (deathmatch->value)
385: self->client->resp.score--;
386: }
387:
388:
389: void Touch_Item (edict_t *ent, edict_t *other, cplane_t *plane, csurface_t *surf);
390:
391: void TossClientWeapon (edict_t *self)
392: {
393: gitem_t *item;
394: edict_t *drop;
395: qboolean quad;
396: float spread;
397:
398: if (!deathmatch->value)
399: return;
400:
401: item = self->client->pers.weapon;
402: if (! self->client->pers.inventory[self->client->ammo_index] )
403: item = NULL;
404: if (item && (strcmp (item->pickup_name, "Blaster") == 0))
405: item = NULL;
406:
407: if (!((int)(dmflags->value) & DF_QUAD_DROP))
408: quad = false;
409: else
410: quad = (self->client->quad_framenum > (level.framenum + 10));
411:
412: if (item && quad)
413: spread = 22.5;
414: else
415: spread = 0.0;
416:
417: if (item)
418: {
419: self->client->v_angle[YAW] -= spread;
420: drop = Drop_Item (self, item);
421: self->client->v_angle[YAW] += spread;
422: drop->spawnflags = DROPPED_PLAYER_ITEM;
423: }
424:
425: if (quad)
426: {
427: self->client->v_angle[YAW] += spread;
428: drop = Drop_Item (self, FindItemByClassname ("item_quad"));
429: self->client->v_angle[YAW] -= spread;
430: drop->spawnflags |= DROPPED_PLAYER_ITEM;
431:
432: drop->touch = Touch_Item;
433: drop->nextthink = level.time + (self->client->quad_framenum - level.framenum) * FRAMETIME;
434: drop->think = G_FreeEdict;
435: }
436: }
437:
438:
439: /*
440: ==================
441: LookAtKiller
442: ==================
443: */
444: void LookAtKiller (edict_t *self, edict_t *inflictor, edict_t *attacker)
445: {
446: vec3_t dir;
447:
448: if (attacker && attacker != world && attacker != self)
449: {
450: VectorSubtract (attacker->s.origin, self->s.origin, dir);
451: }
452: else if (inflictor && inflictor != world && inflictor != self)
453: {
454: VectorSubtract (inflictor->s.origin, self->s.origin, dir);
455: }
456: else
457: {
458: self->client->killer_yaw = self->s.angles[YAW];
459: return;
460: }
461:
462: if (dir[0])
463: self->client->killer_yaw = 180/M_PI*atan2(dir[1], dir[0]);
464: else
465: self->client->killer_yaw = 0;
466: }
467:
468: /*
469: ==================
470: player_die
471: ==================
472: */
473: void player_die (edict_t *self, edict_t *inflictor, edict_t *attacker, int damage, vec3_t point)
474: {
475: int n;
476:
477: VectorClear (self->avelocity);
478:
479: self->takedamage = DAMAGE_YES;
480: self->movetype = MOVETYPE_TOSS;
481:
482: self->s.modelindex2 = 0; // remove linked weapon model
483:
484: self->s.angles[0] = 0;
485: self->s.angles[2] = 0;
486:
487: self->s.sound = 0;
488: self->client->weapon_sound = 0;
489:
490: self->maxs[2] = -8;
491:
492: // self->solid = SOLID_NOT;
493: self->svflags |= SVF_DEADMONSTER;
494:
495: if (!self->deadflag)
496: {
497: self->client->respawn_time = level.time + 1.0;
498: LookAtKiller (self, inflictor, attacker);
499: self->client->ps.pmove.pm_type = PM_DEAD;
500: ClientObituary (self, inflictor, attacker);
501: TossClientWeapon (self);
502: if (deathmatch->value)
503: Cmd_Help_f (self); // show scores
504:
505: // clear inventory
506: // this is kind of ugly, but it's how we want to handle keys in coop
507: for (n = 0; n < game.num_items; n++)
508: {
509: if (coop->value && itemlist[n].flags & IT_KEY)
510: self->client->resp.coop_respawn.inventory[n] = self->client->pers.inventory[n];
511: self->client->pers.inventory[n] = 0;
512: }
513: }
514:
515: // remove powerups
516: self->client->quad_framenum = 0;
517: self->client->invincible_framenum = 0;
518: self->client->breather_framenum = 0;
519: self->client->enviro_framenum = 0;
520: self->flags &= ~FL_POWER_ARMOR;
521:
522: if (self->health < -40)
523: { // gib
524: gi.sound (self, CHAN_BODY, gi.soundindex ("misc/udeath.wav"), 1, ATTN_NORM, 0);
525: for (n= 0; n < 4; n++)
526: ThrowGib (self, "models/objects/gibs/sm_meat/tris.md2", damage, GIB_ORGANIC);
527: ThrowClientHead (self, damage);
528:
529: self->takedamage = DAMAGE_NO;
530: }
531: else
532: { // normal death
533: if (!self->deadflag)
534: {
535: static int i;
536:
537: i = (i+1)%3;
538: // start a death animation
539: self->client->anim_priority = ANIM_DEATH;
540: if (self->client->ps.pmove.pm_flags & PMF_DUCKED)
541: {
542: self->s.frame = FRAME_crdeath1-1;
543: self->client->anim_end = FRAME_crdeath5;
544: }
545: else switch (i)
546: {
547: case 0:
548: self->s.frame = FRAME_death101-1;
549: self->client->anim_end = FRAME_death106;
550: break;
551: case 1:
552: self->s.frame = FRAME_death201-1;
553: self->client->anim_end = FRAME_death206;
554: break;
555: case 2:
556: self->s.frame = FRAME_death301-1;
557: self->client->anim_end = FRAME_death308;
558: break;
559: }
560: gi.sound (self, CHAN_VOICE, gi.soundindex(va("*death%i.wav", (rand()%4)+1)), 1, ATTN_NORM, 0);
561: }
562: }
563:
564: self->deadflag = DEAD_DEAD;
565:
566: gi.linkentity (self);
567: }
568:
569: //=======================================================================
570:
571: /*
572: ==============
573: InitClientPersistant
574:
575: This is only called when the game first initializes in single player,
576: but is called after each death and level change in deathmatch
577: ==============
578: */
579: void InitClientPersistant (gclient_t *client)
580: {
581: gitem_t *item;
582:
583: memset (&client->pers, 0, sizeof(client->pers));
584:
585: item = FindItem("Blaster");
586: client->pers.selected_item = ITEM_INDEX(item);
587: client->pers.inventory[client->pers.selected_item] = 1;
588:
589: client->pers.weapon = item;
590:
591: client->pers.health = 100;
592: client->pers.max_health = 100;
593:
594: client->pers.max_bullets = 200;
595: client->pers.max_shells = 100;
596: client->pers.max_rockets = 50;
597: client->pers.max_grenades = 50;
598: client->pers.max_cells = 200;
599: client->pers.max_slugs = 50;
600:
601: client->pers.connected = true;
602: }
603:
604:
605: void InitClientResp (gclient_t *client)
606: {
607: memset (&client->resp, 0, sizeof(client->resp));
608: client->resp.enterframe = level.framenum;
609: client->resp.coop_respawn = client->pers;
610: }
611:
612: /*
613: ==================
614: SaveClientData
615:
616: Some information that should be persistant, like health,
617: is still stored in the edict structure, so it needs to
618: be mirrored out to the client structure before all the
619: edicts are wiped.
620: ==================
621: */
622: void SaveClientData (void)
623: {
624: int i;
625: edict_t *ent;
626:
627: for (i=0 ; i<game.maxclients ; i++)
628: {
629: ent = &g_edicts[1+i];
630: if (!ent->inuse)
631: continue;
632: game.clients[i].pers.health = ent->health;
633: game.clients[i].pers.max_health = ent->max_health;
634: game.clients[i].pers.savedFlags = (ent->flags & (FL_GODMODE|FL_NOTARGET|FL_POWER_ARMOR));
635: if (coop->value)
636: game.clients[i].pers.score = ent->client->resp.score;
637: }
638: }
639:
640: void FetchClientEntData (edict_t *ent)
641: {
642: ent->health = ent->client->pers.health;
643: ent->max_health = ent->client->pers.max_health;
644: ent->flags |= ent->client->pers.savedFlags;
645: if (coop->value)
646: ent->client->resp.score = ent->client->pers.score;
647: }
648:
649:
650:
651: /*
652: =======================================================================
653:
654: SelectSpawnPoint
655:
656: =======================================================================
657: */
658:
659: /*
660: ================
661: PlayersRangeFromSpot
662:
663: Returns the distance to the nearest player from the given spot
664: ================
665: */
666: float PlayersRangeFromSpot (edict_t *spot)
667: {
668: edict_t *player;
669: float bestplayerdistance;
670: vec3_t v;
671: int n;
672: float playerdistance;
673:
674:
675: bestplayerdistance = 9999999;
676:
677: for (n = 1; n <= maxclients->value; n++)
678: {
679: player = &g_edicts[n];
680:
681: if (!player->inuse)
682: continue;
683:
684: if (player->health <= 0)
685: continue;
686:
687: VectorSubtract (spot->s.origin, player->s.origin, v);
688: playerdistance = VectorLength (v);
689:
690: if (playerdistance < bestplayerdistance)
691: bestplayerdistance = playerdistance;
692: }
693:
694: return bestplayerdistance;
695: }
696:
697: /*
698: ================
699: SelectRandomDeathmatchSpawnPoint
700:
701: go to a random point, but NOT the two points closest
702: to other players
703: ================
704: */
705: edict_t *SelectRandomDeathmatchSpawnPoint (void)
706: {
707: edict_t *spot, *spot1, *spot2;
708: int count = 0;
709: int selection;
710: float range, range1, range2;
711:
712: spot = NULL;
713: range1 = range2 = 99999;
714: spot1 = spot2 = NULL;
715:
716: while ((spot = G_Find (spot, FOFS(classname), "info_player_deathmatch")) != NULL)
717: {
718: count++;
719: range = PlayersRangeFromSpot(spot);
720: if (range < range1)
721: {
722: range1 = range;
723: spot1 = spot;
724: }
725: else if (range < range2)
726: {
727: range2 = range;
728: spot2 = spot;
729: }
730: }
731:
732: if (!count)
733: return NULL;
734:
735: if (count <= 2)
736: {
737: spot1 = spot2 = NULL;
738: }
739: else
740: count -= 2;
741:
742: selection = rand() % count;
743:
744: spot = NULL;
745: do
746: {
747: spot = G_Find (spot, FOFS(classname), "info_player_deathmatch");
748: if (spot == spot1 || spot == spot2)
749: selection++;
750: } while(selection--);
751:
752: return spot;
753: }
754:
755: /*
756: ================
757: SelectFarthestDeathmatchSpawnPoint
758:
759: ================
760: */
761: edict_t *SelectFarthestDeathmatchSpawnPoint (void)
762: {
763: edict_t *bestspot;
764: float bestdistance, bestplayerdistance;
765: edict_t *spot;
766:
767:
768: spot = NULL;
769: bestspot = NULL;
770: bestdistance = 0;
771: while ((spot = G_Find (spot, FOFS(classname), "info_player_deathmatch")) != NULL)
772: {
773: bestplayerdistance = PlayersRangeFromSpot (spot);
774:
775: if (bestplayerdistance > bestdistance)
776: {
777: bestspot = spot;
778: bestdistance = bestplayerdistance;
779: }
780: }
781:
782: if (bestspot)
783: {
784: return bestspot;
785: }
786:
787: // if there is a player just spawned on each and every start spot
788: // we have no choice to turn one into a telefrag meltdown
789: spot = G_Find (NULL, FOFS(classname), "info_player_deathmatch");
790:
791: return spot;
792: }
793:
794: edict_t *SelectDeathmatchSpawnPoint (void)
795: {
796: if ( (int)(dmflags->value) & DF_SPAWN_FARTHEST)
797: return SelectFarthestDeathmatchSpawnPoint ();
798: else
799: return SelectRandomDeathmatchSpawnPoint ();
800: }
801:
802:
803: edict_t *SelectCoopSpawnPoint (edict_t *ent)
804: {
805: int index;
806: edict_t *spot = NULL;
807: char *target;
808:
809: index = ent->client - game.clients;
810:
811: // player 0 starts in normal player spawn point
812: if (!index)
813: return NULL;
814:
815: spot = NULL;
816:
817: // assume there are four coop spots at each spawnpoint
818: while (1)
819: {
820: spot = G_Find (spot, FOFS(classname), "info_player_coop");
821: if (!spot)
822: return NULL; // we didn't have enough...
823:
824: target = spot->targetname;
825: if (!target)
826: target = "";
827: if ( Q_stricmp(game.spawnpoint, target) == 0 )
828: { // this is a coop spawn point for one of the clients here
829: index--;
830: if (!index)
831: return spot; // this is it
832: }
833: }
834:
835:
836: return spot;
837: }
838:
839:
840: /*
841: ===========
842: SelectSpawnPoint
843:
844: Chooses a player start, deathmatch start, coop start, etc
845: ============
846: */
847: void SelectSpawnPoint (edict_t *ent, vec3_t origin, vec3_t angles)
848: {
849: edict_t *spot = NULL;
850:
851: if (deathmatch->value)
852: spot = SelectDeathmatchSpawnPoint ();
853: else if (coop->value)
854: spot = SelectCoopSpawnPoint (ent);
855:
856: // find a single player start spot
857: if (!spot)
858: {
859: while ((spot = G_Find (spot, FOFS(classname), "info_player_start")) != NULL)
860: {
861: if (!game.spawnpoint[0] && !spot->targetname)
862: break;
863:
864: if (!game.spawnpoint[0] || !spot->targetname)
865: continue;
866:
867: if (Q_stricmp(game.spawnpoint, spot->targetname) == 0)
868: break;
869: }
870:
871: if (!spot)
872: {
873: if (!game.spawnpoint[0])
874: { // there wasn't a spawnpoint without a target, so use any
875: spot = G_Find (spot, FOFS(classname), "info_player_start");
876: }
877: if (!spot)
878: gi.error ("Couldn't find spawn point %s\n", game.spawnpoint);
879: }
880: }
881:
882: VectorCopy (spot->s.origin, origin);
883: origin[2] += 9;
884: VectorCopy (spot->s.angles, angles);
885: }
886:
887: //======================================================================
888:
889:
890: void InitBodyQue (void)
891: {
892: int i;
893: edict_t *ent;
894:
895: level.body_que = 0;
896: for (i=0; i<BODY_QUEUE_SIZE ; i++)
897: {
898: ent = G_Spawn();
899: ent->classname = "bodyque";
900: }
901: }
902:
903: void body_die (edict_t *self, edict_t *inflictor, edict_t *attacker, int damage, vec3_t point)
904: {
905: int n;
906:
907: if (self->health < -40)
908: {
909: gi.sound (self, CHAN_BODY, gi.soundindex ("misc/udeath.wav"), 1, ATTN_NORM, 0);
910: for (n= 0; n < 4; n++)
911: ThrowGib (self, "models/objects/gibs/sm_meat/tris.md2", damage, GIB_ORGANIC);
912: self->s.origin[2] -= 48;
913: ThrowClientHead (self, damage);
914: self->takedamage = DAMAGE_NO;
915: }
916: }
917:
918: void CopyToBodyQue (edict_t *ent)
919: {
920: edict_t *body;
921:
922: // grab a body que and cycle to the next one
923: body = &g_edicts[(int)maxclients->value + level.body_que + 1];
924: level.body_que = (level.body_que + 1) % BODY_QUEUE_SIZE;
925:
926: // FIXME: send an effect on the removed body
927:
928: gi.unlinkentity (ent);
929:
930: gi.unlinkentity (body);
931: body->s = ent->s;
932: body->s.number = body - g_edicts;
933:
934: body->svflags = ent->svflags;
935: VectorCopy (ent->mins, body->mins);
936: VectorCopy (ent->maxs, body->maxs);
937: VectorCopy (ent->absmin, body->absmin);
938: VectorCopy (ent->absmax, body->absmax);
939: VectorCopy (ent->size, body->size);
940: body->solid = ent->solid;
941: body->clipmask = ent->clipmask;
942: body->owner = ent->owner;
943: body->movetype = ent->movetype;
944:
945: body->die = body_die;
946: body->takedamage = DAMAGE_YES;
947:
948: gi.linkentity (body);
949: }
950:
951:
952: void respawn (edict_t *self)
953: {
954: if (deathmatch->value || coop->value)
955: {
956: CopyToBodyQue (self);
957: PutClientInServer (self);
958:
959: // add a teleportation effect
960: self->s.event = EV_PLAYER_TELEPORT;
961:
962: // hold in place briefly
963: self->client->ps.pmove.pm_flags = PMF_TIME_TELEPORT;
964: self->client->ps.pmove.pm_time = 14;
965:
966: self->client->respawn_time = level.time;
967:
968: return;
969: }
970:
971: // restart the entire server
972: gi.AddCommandString ("menu_loadgame\n");
973: }
974:
975: //==============================================================
976:
977:
978: /*
979: ===========
980: PutClientInServer
981:
982: Called when a player connects to a server or respawns in
983: a deathmatch.
984: ============
985: */
986: void PutClientInServer (edict_t *ent)
987: {
988: vec3_t mins = {-16, -16, -24};
989: vec3_t maxs = {16, 16, 32};
990: int index;
991: vec3_t spawn_origin, spawn_angles;
992: gclient_t *client;
993: int i;
994: client_persistant_t saved;
995: client_respawn_t resp;
996:
997: // find a spawn point
998: // do it before setting health back up, so farthest
999: // ranging doesn't count this client
1000: SelectSpawnPoint (ent, spawn_origin, spawn_angles);
1001:
1002: index = ent-g_edicts-1;
1003: client = ent->client;
1004:
1005: // deathmatch wipes most client data every spawn
1006: if (deathmatch->value)
1007: {
1008: char userinfo[MAX_INFO_STRING];
1009:
1010: resp = client->resp;
1011: memcpy (userinfo, client->pers.userinfo, sizeof(userinfo));
1012: InitClientPersistant (client);
1013: ClientUserinfoChanged (ent, userinfo);
1014: }
1015: else if (coop->value)
1016: {
1017: // int n;
1018: char userinfo[MAX_INFO_STRING];
1019:
1020: resp = client->resp;
1021: memcpy (userinfo, client->pers.userinfo, sizeof(userinfo));
1022: // this is kind of ugly, but it's how we want to handle keys in coop
1023: // for (n = 0; n < game.num_items; n++)
1024: // {
1025: // if (itemlist[n].flags & IT_KEY)
1026: // resp.coop_respawn.inventory[n] = client->pers.inventory[n];
1027: // }
1028: resp.coop_respawn.game_helpchanged = client->pers.game_helpchanged;
1029: resp.coop_respawn.helpchanged = client->pers.helpchanged;
1030: client->pers = resp.coop_respawn;
1031: ClientUserinfoChanged (ent, userinfo);
1032: if (resp.score > client->pers.score)
1033: client->pers.score = resp.score;
1034: }
1035: else
1036: {
1037: memset (&resp, 0, sizeof(resp));
1038: }
1039:
1040: // clear everything but the persistant data
1041: saved = client->pers;
1042: memset (client, 0, sizeof(*client));
1043: client->pers = saved;
1044: if (client->pers.health <= 0)
1045: InitClientPersistant(client);
1046: client->resp = resp;
1047:
1048: // copy some data from the client to the entity
1049: FetchClientEntData (ent);
1050:
1051: // clear entity values
1052: ent->groundentity = NULL;
1053: ent->client = &game.clients[index];
1054: ent->takedamage = DAMAGE_AIM;
1055: ent->movetype = MOVETYPE_WALK;
1056: ent->viewheight = 22;
1057: ent->inuse = true;
1058: ent->classname = "player";
1059: ent->mass = 200;
1060: ent->solid = SOLID_BBOX;
1061: ent->deadflag = DEAD_NO;
1062: ent->air_finished = level.time + 12;
1063: ent->clipmask = MASK_PLAYERSOLID;
1064: ent->model = "players/male/tris.md2";
1065: ent->pain = player_pain;
1066: ent->die = player_die;
1067: ent->waterlevel = 0;
1068: ent->watertype = 0;
1069: ent->flags &= ~FL_NO_KNOCKBACK;
1070: ent->svflags &= ~SVF_DEADMONSTER;
1071:
1072: VectorCopy (mins, ent->mins);
1073: VectorCopy (maxs, ent->maxs);
1074: VectorClear (ent->velocity);
1075:
1076: // clear playerstate values
1077: memset (&ent->client->ps, 0, sizeof(client->ps));
1078:
1079: client->ps.pmove.origin[0] = spawn_origin[0]*8;
1080: client->ps.pmove.origin[1] = spawn_origin[1]*8;
1081: client->ps.pmove.origin[2] = spawn_origin[2]*8;
1082:
1083: if (deathmatch->value && ((int)dmflags->value & DF_FIXED_FOV))
1084: {
1085: client->ps.fov = 90;
1086: }
1087: else
1088: {
1089: client->ps.fov = atoi(Info_ValueForKey(client->pers.userinfo, "fov"));
1090: if (client->ps.fov < 1)
1091: client->ps.fov = 90;
1092: else if (client->ps.fov > 160)
1093: client->ps.fov = 160;
1094: }
1095:
1096: client->ps.gunindex = gi.modelindex(client->pers.weapon->view_model);
1097:
1098: // clear entity state values
1099: ent->s.effects = 0;
1100: ent->s.modelindex = 255; // will use the skin specified model
1101: ent->s.modelindex2 = 255; // custom gun model
1102: // sknum is player num and weapon number
1103: // weapon number will be added in changeweapon
1104: ent->s.skinnum = ent - g_edicts - 1;
1105:
1106: ent->s.frame = 0;
1107: VectorCopy (spawn_origin, ent->s.origin);
1108: ent->s.origin[2] += 1; // make sure off ground
1109: VectorCopy (ent->s.origin, ent->s.old_origin);
1110:
1111: // set the delta angle
1112: for (i=0 ; i<3 ; i++)
1113: client->ps.pmove.delta_angles[i] = ANGLE2SHORT(spawn_angles[i] - client->resp.cmd_angles[i]);
1114:
1115: ent->s.angles[PITCH] = 0;
1116: ent->s.angles[YAW] = spawn_angles[YAW];
1117: ent->s.angles[ROLL] = 0;
1118: VectorCopy (ent->s.angles, client->ps.viewangles);
1119: VectorCopy (ent->s.angles, client->v_angle);
1120:
1121: if (!KillBox (ent))
1122: { // could't spawn in?
1123: }
1124:
1125: gi.linkentity (ent);
1126:
1127: // force the current weapon up
1128: client->newweapon = client->pers.weapon;
1129: ChangeWeapon (ent);
1130: }
1131:
1132: /*
1133: =====================
1134: ClientBeginDeathmatch
1135:
1136: A client has just connected to the server in
1137: deathmatch mode, so clear everything out before starting them.
1138: =====================
1139: */
1140: void ClientBeginDeathmatch (edict_t *ent)
1141: {
1142: G_InitEdict (ent);
1143:
1144: InitClientResp (ent->client);
1145:
1146: // locate ent at a spawn point
1147: PutClientInServer (ent);
1148:
1149: // send effect
1150: gi.WriteByte (svc_muzzleflash);
1151: gi.WriteShort (ent-g_edicts);
1152: gi.WriteByte (MZ_LOGIN);
1153: gi.multicast (ent->s.origin, MULTICAST_PVS);
1154:
1155: gi.bprintf (PRINT_HIGH, "%s entered the game\n", ent->client->pers.netname);
1156:
1157: // make sure all view stuff is valid
1158: ClientEndServerFrame (ent);
1159: }
1160:
1161:
1162: /*
1163: ===========
1164: ClientBegin
1165:
1166: called when a client has finished connecting, and is ready
1167: to be placed into the game. This will happen every level load.
1168: ============
1169: */
1170: void ClientBegin (edict_t *ent)
1171: {
1172: int i;
1173:
1174: ent->client = game.clients + (ent - g_edicts - 1);
1175:
1176: if (deathmatch->value)
1177: {
1178: ClientBeginDeathmatch (ent);
1179: return;
1180: }
1181:
1182: // if there is already a body waiting for us (a loadgame), just
1183: // take it, otherwise spawn one from scratch
1184: if (ent->inuse == true)
1185: {
1186: // the client has cleared the client side viewangles upon
1187: // connecting to the server, which is different than the
1188: // state when the game is saved, so we need to compensate
1189: // with deltaangles
1190: for (i=0 ; i<3 ; i++)
1191: ent->client->ps.pmove.delta_angles[i] = ANGLE2SHORT(ent->client->ps.viewangles[i]);
1192: }
1193: else
1194: {
1195: // a spawn point will completely reinitialize the entity
1196: // except for the persistant data that was initialized at
1197: // ClientConnect() time
1198: G_InitEdict (ent);
1199: ent->classname = "player";
1200: InitClientResp (ent->client);
1201: PutClientInServer (ent);
1202: }
1203:
1204: if (level.intermissiontime)
1205: {
1206: MoveClientToIntermission (ent);
1207: }
1208: else
1209: {
1210: // send effect if in a multiplayer game
1211: if (game.maxclients > 1)
1212: {
1213: gi.WriteByte (svc_muzzleflash);
1214: gi.WriteShort (ent-g_edicts);
1215: gi.WriteByte (MZ_LOGIN);
1216: gi.multicast (ent->s.origin, MULTICAST_PVS);
1217:
1218: gi.bprintf (PRINT_HIGH, "%s entered the game\n", ent->client->pers.netname);
1219: }
1220: }
1221:
1222: // make sure all view stuff is valid
1223: ClientEndServerFrame (ent);
1224: }
1225:
1226: /*
1227: ===========
1228: ClientUserInfoChanged
1229:
1230: called whenever the player updates a userinfo variable.
1231:
1232: The game can override any of the settings in place
1233: (forcing skins or names, etc) before copying it off.
1234: ============
1235: */
1236: void ClientUserinfoChanged (edict_t *ent, char *userinfo)
1237: {
1238: char *s;
1239: int playernum;
1240:
1241: // check for malformed or illegal info strings
1242: if (!Info_Validate(userinfo))
1243: {
1244: strcpy (userinfo, "\\name\\badinfo\\skin\\male/grunt");
1245: }
1246:
1247: // set name
1248: s = Info_ValueForKey (userinfo, "name");
1249: strncpy (ent->client->pers.netname, s, sizeof(ent->client->pers.netname)-1);
1250:
1251: // set skin
1252: s = Info_ValueForKey (userinfo, "skin");
1253:
1254: playernum = ent-g_edicts-1;
1255:
1256: // combine name and skin into a configstring
1257: gi.configstring (CS_PLAYERSKINS+playernum, va("%s\\%s", ent->client->pers.netname, s) );
1258:
1259: // fov
1260: if (deathmatch->value && ((int)dmflags->value & DF_FIXED_FOV))
1261: {
1262: ent->client->ps.fov = 90;
1263: }
1264: else
1265: {
1266: ent->client->ps.fov = atoi(Info_ValueForKey(userinfo, "fov"));
1267: if (ent->client->ps.fov < 1)
1268: ent->client->ps.fov = 90;
1269: else if (ent->client->ps.fov > 160)
1270: ent->client->ps.fov = 160;
1271: }
1272:
1273: // handedness
1274: s = Info_ValueForKey (userinfo, "hand");
1275: if (strlen(s))
1276: {
1277: ent->client->pers.hand = atoi(s);
1278: }
1279:
1280: // save off the userinfo in case we want to check something later
1281: strncpy (ent->client->pers.userinfo, userinfo, sizeof(ent->client->pers.userinfo)-1);
1282: }
1283:
1284:
1285: /*
1286: ===========
1287: ClientConnect
1288:
1289: Called when a player begins connecting to the server.
1290: The game can refuse entrance to a client by returning false.
1291: If the client is allowed, the connection process will continue
1292: and eventually get to ClientBegin()
1293: Changing levels will NOT cause this to be called again, but
1294: loadgames will.
1295: ============
1296: */
1297: qboolean ClientConnect (edict_t *ent, char *userinfo)
1298: {
1299: char *value;
1300:
1301: // check to see if they are on the banned IP list
1302: value = Info_ValueForKey (userinfo, "ip");
1303: if (SV_FilterPacket(value))
1304: return false;
1305:
1306: // check for a password
1307: value = Info_ValueForKey (userinfo, "password");
1308: if (strcmp(password->string, value) != 0)
1309: return false;
1310:
1311: // they can connect
1312: ent->client = game.clients + (ent - g_edicts - 1);
1313:
1314: // if there is already a body waiting for us (a loadgame), just
1315: // take it, otherwise spawn one from scratch
1316: if (ent->inuse == false)
1317: {
1318: // clear the respawning variables
1319: InitClientResp (ent->client);
1320: if (!game.autosaved || !ent->client->pers.weapon)
1321: InitClientPersistant (ent->client);
1322: }
1323:
1324: ClientUserinfoChanged (ent, userinfo);
1325:
1326: if (game.maxclients > 1)
1327: gi.dprintf ("%s connected\n", ent->client->pers.netname);
1328:
1329: ent->client->pers.connected = true;
1330: return true;
1331: }
1332:
1333: /*
1334: ===========
1335: ClientDisconnect
1336:
1337: Called when a player drops from the server.
1338: Will not be called between levels.
1339: ============
1340: */
1341: void ClientDisconnect (edict_t *ent)
1342: {
1343: int playernum;
1344:
1345: if (!ent->client)
1346: return;
1347:
1348: gi.bprintf (PRINT_HIGH, "%s disconnected\n", ent->client->pers.netname);
1349:
1350: // send effect
1351: gi.WriteByte (svc_muzzleflash);
1352: gi.WriteShort (ent-g_edicts);
1353: gi.WriteByte (MZ_LOGOUT);
1354: gi.multicast (ent->s.origin, MULTICAST_PVS);
1355:
1356: gi.unlinkentity (ent);
1357: ent->s.modelindex = 0;
1358: ent->solid = SOLID_NOT;
1359: ent->inuse = false;
1360: ent->classname = "disconnected";
1361: ent->client->pers.connected = false;
1362:
1363: playernum = ent-g_edicts-1;
1364: gi.configstring (CS_PLAYERSKINS+playernum, "");
1365: }
1366:
1367:
1368: //==============================================================
1369:
1370:
1371: edict_t *pm_passent;
1372:
1373: // pmove doesn't need to know about passent and contentmask
1374: trace_t PM_trace (vec3_t start, vec3_t mins, vec3_t maxs, vec3_t end)
1375: {
1376: if (pm_passent->health > 0)
1377: return gi.trace (start, mins, maxs, end, pm_passent, MASK_PLAYERSOLID);
1378: else
1379: return gi.trace (start, mins, maxs, end, pm_passent, MASK_DEADSOLID);
1380: }
1381:
1382: unsigned CheckBlock (void *b, int c)
1383: {
1384: int v,i;
1385: v = 0;
1386: for (i=0 ; i<c ; i++)
1387: v+= ((byte *)b)[i];
1388: return v;
1389: }
1390: void PrintPmove (pmove_t *pm)
1391: {
1392: unsigned c1, c2;
1393:
1394: c1 = CheckBlock (&pm->s, sizeof(pm->s));
1395: c2 = CheckBlock (&pm->cmd, sizeof(pm->cmd));
1396: Com_Printf ("sv %3i:%i %i\n", pm->cmd.impulse, c1, c2);
1397: }
1398:
1399: /*
1400: ==============
1401: ClientThink
1402:
1403: This will be called once for each client frame, which will
1404: usually be a couple times for each server frame.
1405: ==============
1406: */
1407: void ClientThink (edict_t *ent, usercmd_t *ucmd)
1408: {
1409: gclient_t *client;
1410: edict_t *other;
1411: int i, j;
1412: pmove_t pm;
1413:
1414: level.current_entity = ent;
1415: client = ent->client;
1416:
1417: if (level.intermissiontime)
1418: {
1419: client->ps.pmove.pm_type = PM_FREEZE;
1420: // can exit intermission after five seconds
1421: if (level.time > level.intermissiontime + 5.0
1422: && (ucmd->buttons & BUTTON_ANY) )
1423: level.exitintermission = true;
1424: return;
1425: }
1426:
1427: pm_passent = ent;
1428:
1429: // set up for pmove
1430: memset (&pm, 0, sizeof(pm));
1431:
1432: if (ent->movetype == MOVETYPE_NOCLIP)
1433: client->ps.pmove.pm_type = PM_SPECTATOR;
1434: else if (ent->s.modelindex != 255)
1435: client->ps.pmove.pm_type = PM_GIB;
1436: else if (ent->deadflag)
1437: client->ps.pmove.pm_type = PM_DEAD;
1438: else
1439: client->ps.pmove.pm_type = PM_NORMAL;
1440:
1441: client->ps.pmove.gravity = sv_gravity->value;
1442: pm.s = client->ps.pmove;
1443:
1444: for (i=0 ; i<3 ; i++)
1445: {
1446: pm.s.origin[i] = ent->s.origin[i]*8;
1447: pm.s.velocity[i] = ent->velocity[i]*8;
1448: }
1449:
1450: if (memcmp(&client->old_pmove, &pm.s, sizeof(pm.s)))
1451: {
1452: pm.snapinitial = true;
1453: // gi.dprintf ("pmove changed!\n");
1454: }
1455:
1456: pm.cmd = *ucmd;
1457:
1458: pm.trace = PM_trace; // adds default parms
1459: pm.pointcontents = gi.pointcontents;
1460:
1461: // perform a pmove
1462: gi.Pmove (&pm);
1463:
1464: // save results of pmove
1465: client->ps.pmove = pm.s;
1466: client->old_pmove = pm.s;
1467:
1468: for (i=0 ; i<3 ; i++)
1469: {
1470: ent->s.origin[i] = pm.s.origin[i]*0.125;
1471: ent->velocity[i] = pm.s.velocity[i]*0.125;
1472: }
1473:
1474: VectorCopy (pm.mins, ent->mins);
1475: VectorCopy (pm.maxs, ent->maxs);
1476:
1477: client->resp.cmd_angles[0] = SHORT2ANGLE(ucmd->angles[0]);
1478: client->resp.cmd_angles[1] = SHORT2ANGLE(ucmd->angles[1]);
1479: client->resp.cmd_angles[2] = SHORT2ANGLE(ucmd->angles[2]);
1480:
1481: if (ent->groundentity && !pm.groundentity && (pm.cmd.upmove >= 10) && (pm.waterlevel == 0))
1482: {
1483: gi.sound(ent, CHAN_VOICE, gi.soundindex("*jump1.wav"), 1, ATTN_NORM, 0);
1484: PlayerNoise(ent, ent->s.origin, PNOISE_SELF);
1485: }
1486:
1487: ent->viewheight = pm.viewheight;
1488: ent->waterlevel = pm.waterlevel;
1489: ent->watertype = pm.watertype;
1490: ent->groundentity = pm.groundentity;
1491: if (pm.groundentity)
1492: ent->groundentity_linkcount = pm.groundentity->linkcount;
1493:
1494: if (ent->deadflag)
1495: {
1496: client->ps.viewangles[ROLL] = 40;
1497: client->ps.viewangles[PITCH] = -15;
1498: client->ps.viewangles[YAW] = client->killer_yaw;
1499: }
1500: else
1501: {
1502: VectorCopy (pm.viewangles, client->v_angle);
1503: VectorCopy (pm.viewangles, client->ps.viewangles);
1504: }
1505:
1506: gi.linkentity (ent);
1507:
1508: if (ent->movetype != MOVETYPE_NOCLIP)
1509: G_TouchTriggers (ent);
1510:
1511: // touch other objects
1512: for (i=0 ; i<pm.numtouch ; i++)
1513: {
1514: other = pm.touchents[i];
1515: for (j=0 ; j<i ; j++)
1516: if (pm.touchents[j] == other)
1517: break;
1518: if (j != i)
1519: continue; // duplicated
1520: if (!other->touch)
1521: continue;
1522: other->touch (other, ent, NULL, NULL);
1523: }
1524:
1525:
1526: client->oldbuttons = client->buttons;
1527: client->buttons = ucmd->buttons;
1528: client->latched_buttons |= client->buttons & ~client->oldbuttons;
1529:
1530: // save light level the player is standing on for
1531: // monster sighting AI
1532: ent->light_level = ucmd->lightlevel;
1533:
1534: // fire weapon from final position if needed
1535: if (client->latched_buttons & BUTTON_ATTACK)
1536: {
1537: if (!client->weapon_thunk)
1538: {
1539: client->weapon_thunk = true;
1540: Think_Weapon (ent);
1541: }
1542: }
1543:
1544:
1545: }
1546:
1547:
1548: /*
1549: ==============
1550: ClientBeginServerFrame
1551:
1552: This will be called once for each server frame, before running
1553: any other entities in the world.
1554: ==============
1555: */
1556: void ClientBeginServerFrame (edict_t *ent)
1557: {
1558: gclient_t *client;
1559: int buttonMask;
1560:
1561: if (level.intermissiontime)
1562: return;
1563:
1564: client = ent->client;
1565:
1566: // run weapon animations if it hasn't been done by a ucmd_t
1567: if (!client->weapon_thunk)
1568: Think_Weapon (ent);
1569: else
1570: client->weapon_thunk = false;
1571:
1572: if (ent->deadflag)
1573: {
1574: // wait for any button just going down
1575: if ( level.time > client->respawn_time)
1576: {
1577: // in deathmatch, only wait for attack button
1578: if (deathmatch->value)
1579: buttonMask = BUTTON_ATTACK;
1580: else
1581: buttonMask = -1;
1582:
1583: if ( ( client->latched_buttons & buttonMask ) ||
1584: (deathmatch->value && ((int)dmflags->value & DF_FORCE_RESPAWN) ) )
1585: {
1586: respawn(ent);
1587: client->latched_buttons = 0;
1588: }
1589: }
1590: return;
1591: }
1592:
1593: // add player trail so monsters can follow
1594: if (!deathmatch->value)
1595: if (!visible (ent, PlayerTrail_LastSpot() ) )
1596: PlayerTrail_Add (ent->s.old_origin);
1597:
1598: client->latched_buttons = 0;
1599: }
This archive runs on limited infrastructure. Preserving old code on modern bandwidth. Automated agents are requested to crawl responsibly.