1 /* bzflag
2  * Copyright (c) 1993-2021 Tim Riker
3  *
4  * This package is free software;  you can redistribute it and/or
5  * modify it under the terms of the license found in the file
6  * named COPYING that should have accompanied this file.
7  *
8  * THIS PACKAGE IS PROVIDED ``AS IS'' AND WITHOUT ANY EXPRESS OR
9  * IMPLIED WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED
10  * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.
11  */
12 
13 /* interface header */
14 #include "PlayerInfo.h"
15 
16 /* system implementation headers */
17 #include <errno.h>
18 #include <stdio.h>
19 #include <string>
20 #include <ctype.h>
21 
22 /* implementation-specific common headers */
23 #include "TextUtils.h"
24 
25 WordFilter PlayerInfo::serverSpoofingFilter;
26 TimeKeeper PlayerInfo::now = TimeKeeper::getCurrent();
27 
28 bool    PlayerInfo::callSignFiltering = false;
29 WordFilter *PlayerInfo::filterData  = NULL;
30 bool    PlayerInfo::simpleFiltering   = true;
31 
PlayerInfo(int _playerIndex)32 PlayerInfo::PlayerInfo(int _playerIndex) :
33     playerIndex(_playerIndex),
34     state(PlayerInLimbo),
35     hasDoneEntering(false),
36     team(NoTeam),
37     flag(-1),
38     allowedToSpawn(true),
39     notifiedSpawn(false),
40     spamWarns(0),
41     lastMsgTime(now),
42     paused(false),
43     pausedSince(TimeKeeper::getNullTime()),
44     autopilot(false),
45     tracker(0)
46 {
47     notResponding = false;
48     memset(motto, 0, MottoLen);
49     memset(callSign, 0, CallSignLen);
50     memset(token, 0, TokenLen);
51     memset(clientVersion, 0, VersionLen);
52     clientVersionMajor = -1;
53     clientVersionMinor = -1;
54     clientVersionRevision = -1;
55     completelyAdded = false;
56     nextSpawnTime = TimeKeeper::getSunGenesisTime();
57     wantsToSpawn = false;
58     neverSpawned = true;
59     type = TankPlayer;
60 }
61 
setFilterParameters(bool _callSignFiltering,WordFilter & _filterData,bool _simpleFiltering)62 void PlayerInfo::setFilterParameters(bool   _callSignFiltering,
63                                      WordFilter &_filterData,
64                                      bool   _simpleFiltering)
65 {
66     callSignFiltering = _callSignFiltering;
67     filterData    = &_filterData;
68     simpleFiltering   = _simpleFiltering;
69 }
70 
resetPlayer(bool ctf)71 void PlayerInfo::resetPlayer(bool ctf)
72 {
73     wasRabbit = false;
74 
75     lastupdate = now;
76     lastmsg    = now;
77 
78     replayState = ReplayNone;
79 
80     playedEarly = false;
81 
82     restartOnBase = ctf;
83 }
84 
setRestartOnBase(bool on)85 void PlayerInfo::setRestartOnBase(bool on)
86 {
87     restartOnBase = on;
88 }
89 
shouldRestartAtBase()90 bool PlayerInfo::shouldRestartAtBase()
91 {
92     return restartOnBase;
93 }
94 
signingOn()95 void PlayerInfo::signingOn()
96 {
97     state = PlayerDead;
98 }
99 
setAlive()100 void PlayerInfo::setAlive()
101 {
102     state = PlayerAlive;
103     paused = false;
104     wantsToSpawn = false;
105     neverSpawned = false;
106     flag = -1;
107 }
108 
setDead()109 void PlayerInfo::setDead()
110 {
111     state = PlayerDead;
112 }
113 
packUpdate(void * buf)114 void *PlayerInfo::packUpdate(void *buf)
115 {
116     buf = nboPackUShort(buf, uint16_t(type));
117     buf = nboPackUShort(buf, uint16_t(team));
118     return buf;
119 }
120 
packId(void * buf)121 void *PlayerInfo::packId(void *buf)
122 {
123     buf = nboPackString(buf, callSign, CallSignLen);
124     buf = nboPackString(buf, motto, MottoLen);
125     return buf;
126 }
127 
unpackEnter(const void * buf,uint16_t & rejectCode,char * rejectMsg)128 bool PlayerInfo::unpackEnter(const void *buf, uint16_t &rejectCode, char *rejectMsg)
129 {
130     // data: type, team, name, motto
131     uint16_t _type;
132     int16_t _team;
133     buf = nboUnpackUShort(buf, _type);
134     buf = nboUnpackShort(buf, _team);
135     type = PlayerType(_type);
136     team = TeamColor(_team);
137     buf = nboUnpackString(buf, callSign, CallSignLen);
138     buf = nboUnpackString(buf, motto, MottoLen);
139     buf = nboUnpackString(buf, token, TokenLen);
140     buf = nboUnpackString(buf, clientVersion, VersionLen);
141 
142     return processEnter(rejectCode, rejectMsg);
143 }
144 
processEnter(uint16_t & rejectCode,char * rejectMsg)145 bool PlayerInfo::processEnter ( uint16_t &rejectCode, char *rejectMsg )
146 {
147     // terminate the strings
148     callSign[CallSignLen - 1] = '\0';
149     motto[MottoLen - 1] = '\0';
150     token[TokenLen - 1] = '\0';
151     clientVersion[VersionLen - 1] = '\0';
152     cleanMotto();
153 
154     logDebugMessage(2,"Player %s [%d] sent version string: %s\n",
155                     callSign, playerIndex, clientVersion);
156     int major, minor, rev;
157     if (sscanf(clientVersion, "%d.%d.%d", &major, &minor, &rev) == 3)
158     {
159         clientVersionMajor = major;
160         clientVersionMinor = minor;
161         clientVersionRevision = rev;
162     }
163     logDebugMessage(4,"Player %s version code parsed as:  %i.%i.%i\n", callSign,
164                     clientVersionMajor, clientVersionMinor, clientVersionRevision);
165 
166     // spoof filter holds "SERVER" for robust name comparisons
167     if (serverSpoofingFilter.wordCount() == 0)
168         serverSpoofingFilter.addToFilter("SERVER", "");
169 
170     if (isBot() && BZDB.isTrue(StateDatabase::BZDB_DISABLEBOTS))
171     {
172         logDebugMessage(2,"rejecting robot tank: %s\n", callSign);
173         rejectCode   = RejectBadType;
174         strcpy(rejectMsg, "Robot tanks are not allowed on this server.");
175         return false;
176     }
177 
178     if (!isCallSignReadable())
179     {
180         logDebugMessage(2,"rejecting unreadable callsign: %s\n", callSign);
181         rejectCode   = RejectBadCallsign;
182         strcpy(rejectMsg, errorString.c_str());
183         return false;
184     }
185     // no spoofing the server name
186     if (serverSpoofingFilter.filter(callSign))
187     {
188         rejectCode   = RejectRepeatCallsign;
189         strcpy(rejectMsg, "The callsign specified is already in use.");
190         return false;
191     }
192     if (!isMottoReadable())
193     {
194         logDebugMessage(2,"ignoring unreadable player motto: %s (%s)\n", callSign, motto);
195         // The sendMessage() function is not available in this scope.
196         // TODO: reorganize the code to send this courtesy notice to the player.
197         // sendMessage(ServerPlayer, playerIndex, "Ignoring your unreadable motto.");
198         setMotto("");
199     }
200 
201     // make sure the callsign is not obscene/filtered
202     if (callSignFiltering)
203     {
204         logDebugMessage(2,"checking callsign: %s\n",callSign);
205 
206         char cs[CallSignLen];
207         memcpy(cs, callSign, sizeof(char) * CallSignLen);
208         if (filterData->filter(cs, simpleFiltering))
209         {
210             rejectCode = RejectBadCallsign;
211             strcpy(rejectMsg,
212                    "The callsign was rejected. Try a different callsign.");
213             return false;
214         }
215     }
216 
217     // make sure the motto is not obscene/filtered
218     if (callSignFiltering)
219     {
220         logDebugMessage(2,"checking motto: %s\n", motto);
221         char em[MottoLen];
222         memcpy(em, motto, sizeof(char) * MottoLen);
223         if (filterData->filter(em, simpleFiltering))
224         {
225             rejectCode = RejectBadMotto;
226             strcpy(rejectMsg, "The motto was rejected. Try a different motto.");
227             return false;
228         }
229     }
230 
231     if (token[0] == 0)
232         strcpy(token, "NONE");
233     hasDoneEntering = true;
234     return true;
235 }
236 
getCallSign() const237 const char *PlayerInfo::getCallSign() const
238 {
239     return callSign;
240 }
241 
setCallSign(const char * c)242 void PlayerInfo::setCallSign(const char * c)
243 {
244     if (c != NULL)
245     {
246         strncpy(callSign, c, CallSignLen);
247         callSign[CallSignLen - 1] = '\0';   // ensure null termination
248     }
249 }
250 
251 
isCallSignReadable()252 bool PlayerInfo::isCallSignReadable()
253 {
254     // callsign readability filter, make sure there are more alphanum than non
255     // keep a count of alpha-numerics
256 
257     int callsignlen = (int)strlen(callSign);
258     // reject less than 2 characters
259     if (callsignlen < 2)
260     {
261         errorString = "Callsigns must be at least 2 characters.";
262         return false;
263     }
264 
265     // reject trailing space
266     if (isspace(callSign[strlen(callSign) - 1]))
267     {
268         errorString = "Trailing spaces are not allowed in callsigns.";
269         return false;
270     }
271 
272     // prevent spoofing global login indicators + and @ in the scoreboard
273     // and reserve # for /kick or /ban #slot
274     if (*callSign=='+' || *callSign=='@' || *callSign=='#' )
275     {
276         errorString = "Callsigns are not allowed to start with +, @, or #.";
277         return false;
278     }
279 
280     // start with true to reject leading space
281     bool lastWasSpace = true;
282     int alnumCount = 0;
283     const char *sp = callSign;
284     do
285     {
286         // reject sequential spaces
287         if (lastWasSpace && isspace(*sp))
288         {
289             errorString = "Leading or consecutive spaces are not allowed in callsigns.";
290             return false;
291         }
292 
293         // reject ' and " and any nonprintable
294         if ((*sp == '\'') || (*sp == '"') || ((unsigned)*sp > 0x7f) || !isprint(*sp))
295         {
296             errorString = "Non-printable characters and quotes are not allowed in callsigns.";
297             return false;
298         }
299         if (isspace(*sp))
300         {
301             // only space is valid, not tab etc.
302             if (*sp != ' ')
303             {
304                 errorString = "Invalid whitespace in callsign.";
305                 return false;
306             }
307             lastWasSpace = true;
308         }
309         else
310         {
311             lastWasSpace = false;
312             if (isalnum(*sp))
313                 alnumCount++;
314         }
315     }
316     while (*++sp);
317 
318     bool readable = ((float)alnumCount / (float)callsignlen) > 0.5f;
319     if (!readable)
320         errorString = "Callsign rejected. Please use mostly letters and numbers.";
321     return readable;
322 }
323 
getMotto() const324 const char *PlayerInfo::getMotto() const
325 {
326     return motto;
327 }
328 
cleanMotto()329 void PlayerInfo::cleanMotto()
330 {
331     // strip leading whitespace from motto
332     char *sp = motto;
333     char *tp = sp;
334     while (isspace(*sp))
335         sp++;
336 
337     // strip any non-printable characters and ' and " from motto
338     do
339     {
340         if (isprint(*sp) && (*sp != '\'') && (*sp != '"'))
341             *tp++ = *sp;
342     }
343     while (*++sp);
344     *tp = *sp;
345 
346     // strip trailing whitespace from motto
347     while (isspace(*--tp))
348         *tp=0;
349 }
350 
isMottoReadable()351 bool PlayerInfo::isMottoReadable()
352 {
353     // motto/"team" readability filter, make sure there are more
354     // alphanum than non
355     int mottoAlnumCount = 0;
356     char *sp = motto;
357     do
358     {
359         if (isalnum(*sp))
360             mottoAlnumCount++;
361     }
362     while (*++sp);
363     int mottolen = (int)strlen(motto);
364     return (mottolen <= 4) || (((float)mottoAlnumCount / (float)mottolen) > 0.5);
365 }
366 
setMotto(const char * _motto)367 void PlayerInfo::setMotto(const char* _motto)
368 {
369     strncpy(motto, _motto, MottoLen);
370     motto[MottoLen - 1] = '\0';   // ensure null termination
371 }
372 
getToken() const373 const char *PlayerInfo::getToken() const
374 {
375     return token;
376 }
377 
setToken(const char * c)378 void PlayerInfo::setToken(const char * c)
379 {
380     if (c != NULL)
381     {
382         strncpy(token, c, TokenLen);
383         token[TokenLen - 1] = '\0'; // ensure null termination
384     }
385 }
386 
clearToken()387 void PlayerInfo::clearToken()
388 {
389     token[0] = '\0';
390 }
391 
packVirtualFlagCapture(void * buf)392 void *PlayerInfo::packVirtualFlagCapture(void *buf)
393 {
394     buf = nboPackUShort(buf, uint16_t(int(team) - 1));
395     buf = nboPackUShort(buf, uint16_t(1 + (int(team) % 4)));
396     return buf;
397 }
398 
isTeam(TeamColor _team) const399 bool PlayerInfo::isTeam(TeamColor _team) const
400 {
401     return team == _team;
402 }
403 
isObserver() const404 bool PlayerInfo::isObserver() const
405 {
406     return team == ObserverTeam;
407 }
408 
getTeam() const409 TeamColor PlayerInfo::getTeam() const
410 {
411     return team;
412 }
413 
setTeam(TeamColor _team)414 void PlayerInfo::setTeam(TeamColor _team)
415 {
416     team = _team;
417 }
418 
wasARabbit()419 void PlayerInfo::wasARabbit()
420 {
421     team = HunterTeam;
422     wasRabbit = true;
423 }
424 
wasNotARabbit()425 void PlayerInfo::wasNotARabbit()
426 {
427     wasRabbit = false;
428 }
429 
resetFlag()430 void PlayerInfo::resetFlag()
431 {
432     flag = -1;
433     lastFlagDropTime = now;
434 }
435 
setFlag(int _flag)436 void PlayerInfo::setFlag(int _flag)
437 {
438     flag = _flag;
439 }
440 
isFlagTransitSafe()441 bool PlayerInfo::isFlagTransitSafe()
442 {
443     return now - lastFlagDropTime >= 2.0f;
444 }
445 
timeSinceLastFlagDrop()446 double PlayerInfo::timeSinceLastFlagDrop()
447 {
448     return now - lastFlagDropTime;
449 }
450 
getClientVersion()451 const char *PlayerInfo::getClientVersion()
452 {
453     return clientVersion;
454 }
455 
setClientVersion(const char * c)456 void PlayerInfo::setClientVersion(const char * c)
457 {
458     if (c != NULL)
459     {
460         strncpy(clientVersion, c, VersionLen);
461         clientVersion[VersionLen - 1] = '\0';   // ensure null termination
462     }
463 }
464 
getClientVersionNumbers(int & major,int & minor,int & rev)465 void PlayerInfo::getClientVersionNumbers(int& major, int& minor, int& rev)
466 {
467     major = clientVersionMajor;
468     minor = clientVersionMinor;
469     rev = clientVersionRevision;
470     return;
471 }
472 
getPausedTime()473 double PlayerInfo::getPausedTime()
474 {
475     if ((state > PlayerInLimbo) && (team != ObserverTeam) && (paused))
476         return (now - pausedSince);
477 
478     return 0;
479 }
480 
getIdleTime()481 double PlayerInfo::getIdleTime()
482 {
483     if ((state > PlayerInLimbo) && (team != ObserverTeam))
484         return (now - lastupdate);
485     else if (team == ObserverTeam)
486         return -1;
487 
488     return 0;
489 }
490 
getIdleStat()491 std::string PlayerInfo::getIdleStat()
492 {
493     std::string reply;
494     if ((state > PlayerInLimbo) && (team != ObserverTeam))
495     {
496         reply = TextUtils::format("%s\t: %4ds", callSign,
497                                   int(now - lastupdate));
498         if (paused)
499             reply += TextUtils::format("  paused %4ds", int(now - pausedSince));
500     }
501     return reply;
502 }
503 
canBeRabbit(bool relaxing)504 bool PlayerInfo::canBeRabbit(bool relaxing)
505 {
506     if (paused || notResponding || (team == ObserverTeam))
507         return false;
508     return relaxing ? (state > PlayerInLimbo) : (state == PlayerAlive);
509 }
510 
setPaused(bool _paused)511 void PlayerInfo::setPaused(bool _paused)
512 {
513     paused = _paused;
514     pausedSince = now;
515 }
516 
setAutoPilot(bool _autopilot)517 void PlayerInfo::setAutoPilot(bool _autopilot)
518 {
519     autopilot = _autopilot;
520 }
521 
isTooMuchIdling(float kickThresh)522 bool PlayerInfo::isTooMuchIdling(float kickThresh)
523 {
524     bool idling = false;
525     if ((state > PlayerInLimbo) && (team != ObserverTeam))
526     {
527         const float idletime = (float)(now - lastupdate);
528         if (idletime > kickThresh)
529             idling = true;
530     }
531     return idling;
532 }
533 
hasStartedToNotRespond()534 bool PlayerInfo::hasStartedToNotRespond()
535 {
536     const float notRespondingTime =
537         BZDB.eval(StateDatabase::BZDB_NOTRESPONDINGTIME);
538     bool startingToNotRespond = false;
539     if (state > PlayerInLimbo)
540     {
541         bool oldnr = notResponding;
542         notResponding = (now - lastupdate) > notRespondingTime;
543         if (!oldnr && notResponding)
544             startingToNotRespond = true;
545     }
546     return startingToNotRespond;
547 }
548 
hasSent()549 void PlayerInfo::hasSent()
550 {
551     lastmsg = now;
552 }
553 
hasPlayedEarly()554 bool PlayerInfo::hasPlayedEarly()
555 {
556     bool returnValue = playedEarly;
557     playedEarly      = false;
558     return returnValue;
559 }
560 
setPlayedEarly(bool early)561 void PlayerInfo::setPlayedEarly(bool early)
562 {
563     playedEarly = early;
564 }
565 
updateIdleTime()566 void PlayerInfo::updateIdleTime()
567 {
568     if (!paused && (state != PlayerDead))
569         lastupdate = now;
570 }
571 
setReplayState(PlayerReplayState _state)572 void    PlayerInfo::setReplayState(PlayerReplayState _state)
573 {
574     replayState = _state;
575 }
576 
getReplayState()577 PlayerReplayState PlayerInfo::getReplayState()
578 {
579     return replayState;
580 }
581 
582 
setTrackerID(unsigned short int t)583 void PlayerInfo::setTrackerID(unsigned short int t)
584 {
585     tracker = t;
586 }
587 
588 
trackerID()589 unsigned short int PlayerInfo::trackerID()
590 {
591     return tracker;
592 }
593 
setCurrentTime(TimeKeeper tm)594 void PlayerInfo::setCurrentTime(TimeKeeper tm)
595 {
596     now = tm;
597 }
598 
killedBy(PlayerId killer)599 void PlayerInfo::killedBy(PlayerId killer)
600 {
601     if (deathCountMap.find(killer) == deathCountMap.end())
602         deathCountMap[killer] = 0;
603     deathCountMap[killer]++;
604 }
605 
flushKiller(PlayerId killer)606 void PlayerInfo::flushKiller(PlayerId killer)
607 {
608     if (deathCountMap.find(killer) != deathCountMap.end())
609         deathCountMap.erase(killer);
610 }
611 
howManyTimesKilledBy(PlayerId killer)612 int PlayerInfo::howManyTimesKilledBy(PlayerId killer)
613 {
614     if (deathCountMap.find(killer) == deathCountMap.end())
615         return 0;
616     return deathCountMap[killer];
617 }
618 
setAllowedToSpawn(bool canSpawn)619 void PlayerInfo::setAllowedToSpawn(bool canSpawn)
620 {
621     allowedToSpawn = canSpawn;
622 }
623 
setNotifiedOfSpawnable(bool notified)624 void PlayerInfo::setNotifiedOfSpawnable(bool notified)
625 {
626     notifiedSpawn = notified;
627 }
628 
629 // Local Variables: ***
630 // mode: C++ ***
631 // tab-width: 4 ***
632 // c-basic-offset: 4 ***
633 // indent-tabs-mode: nil ***
634 // End: ***
635 // ex: shiftwidth=4 tabstop=4
636