]> git.xonotic.org Git - xonotic/xonotic-data.pk3dir.git/blob - qcsrc/server/chat.qc
Fix string length checks in world.qc and add VM_TEMPSTRING_MAXSIZE
[xonotic/xonotic-data.pk3dir.git] / qcsrc / server / chat.qc
1 #include "chat.qh"
2
3 #include <common/gamemodes/_mod.qh>
4 #include <common/mapobjects/target/location.qh>
5 #include <common/mapobjects/triggers.qh>
6 #include <common/notifications/all.qh>
7 #include <common/teams.qh>
8 #include <common/util.qh>
9 #include <common/weapons/weapon.qh>
10 #include <common/wepent.qh>
11 #include <server/command/cmd.qh>
12 #include <server/command/common.qh>
13 #include <server/gamelog.qh>
14 #include <server/main.qh>
15 #include <server/mapvoting.qh>
16 #include <server/mutators/_mod.qh>
17 #include <server/weapons/tracing.qh>
18 #include <server/world.qh>
19
20 /**
21  * message "": do not say, just test flood control
22  * return value:
23  *   1 = accept
24  *   0 = reject
25  *  -1 = fake accept
26  */
27 int Say(entity source, int teamsay, entity privatesay, string msgin, bool floodcontrol)
28 {
29         if(!autocvar_g_chat_allowed && IS_REAL_CLIENT(source))
30         {
31                 Send_Notification(NOTIF_ONE_ONLY, source, MSG_INFO, INFO_CHAT_DISABLED);
32                 return 0;
33         }
34
35         if(!autocvar_g_chat_private_allowed && privatesay)
36         {
37                 Send_Notification(NOTIF_ONE_ONLY, source, MSG_INFO, INFO_CHAT_PRIVATE_DISABLED);
38                 return 0;
39         }
40
41         if(!autocvar_g_chat_spectator_allowed && IS_OBSERVER(source))
42         {
43                 Send_Notification(NOTIF_ONE_ONLY, source, MSG_INFO, INFO_CHAT_SPECTATOR_DISABLED);
44                 return 0;
45         }
46
47         if(!autocvar_g_chat_team_allowed && teamsay)
48         {
49                 Send_Notification(NOTIF_ONE_ONLY, source, MSG_INFO, INFO_CHAT_TEAM_DISABLED);
50                 return 0;
51         }
52
53         if (!teamsay && !privatesay && substring(msgin, 0, 1) == " ")
54                 msgin = substring(msgin, 1, -1); // work around DP say bug (say_team does not have this!)
55
56         if (source)
57                 msgin = formatmessage(source, msgin);
58
59         string colorstr;
60         if (!(IS_PLAYER(source) || INGAME(source)))
61                 colorstr = "^0"; // black for spectators
62         else if(teamplay)
63                 colorstr = Team_ColorCode(source.team);
64         else
65         {
66                 colorstr = "";
67                 teamsay = false;
68         }
69
70         if (!source) {
71                 colorstr = "";
72                 teamsay = false;
73         }
74
75         if(msgin != "")
76                 msgin = trigger_magicear_processmessage_forallears(source, teamsay, privatesay, msgin);
77
78         /*
79          * using bprint solves this... me stupid
80         // how can we prevent the message from appearing in a listen server?
81         // for now, just give "say" back and only handle say_team
82         if(!teamsay)
83         {
84                 clientcommand(source, strcat("say ", msgin));
85                 return;
86         }
87         */
88
89         string namestr = "";
90         if (source)
91                 namestr = playername(source.netname, source.team, (autocvar_g_chat_teamcolors && IS_PLAYER(source)));
92
93         string colorprefix = (strdecolorize(namestr) == namestr) ? "^3" : "^7";
94
95         string msgstr = "", cmsgstr = "";
96         string privatemsgprefix = string_null;
97         int privatemsgprefixlen = 0;
98         if (msgin != "")
99         {
100                 bool found_me = false;
101                 if(strstrofs(msgin, "/me", 0) >= 0)
102                 {
103                         string newmsgin = "";
104                         string newnamestr = ((teamsay) ? strcat(colorstr, "(", colorprefix, namestr, colorstr, ")", "^7") : strcat(colorprefix, namestr, "^7"));
105                         FOREACH_WORD(msgin, true,
106                         {
107                                 if(strdecolorize(it) == "/me")
108                                 {
109                                         found_me = true;
110                                         newmsgin = cons(newmsgin, newnamestr);
111                                 }
112                                 else
113                                         newmsgin = cons(newmsgin, it);
114                         });
115                         msgin = newmsgin;
116                 }
117
118                 if(privatesay)
119                 {
120                         msgstr = strcat("\{1}\{13}* ", colorprefix, namestr, "^3 tells you: ^7");
121                         privatemsgprefixlen = strlen(msgstr);
122                         msgstr = strcat(msgstr, msgin);
123                         cmsgstr = strcat(colorstr, colorprefix, namestr, "^3 tells you:\n^7", msgin);
124                         privatemsgprefix = strcat("\{1}\{13}* ^3You tell ", playername(privatesay.netname, privatesay.team, (autocvar_g_chat_teamcolors && IS_PLAYER(privatesay))), ": ^7");
125                 }
126                 else if(teamsay)
127                 {
128                         if(found_me)
129                         {
130                                 //msgin = strreplace("/me", "", msgin);
131                                 //msgin = substring(msgin, 3, strlen(msgin));
132                                 //msgin = strreplace("/me", strcat(colorstr, "(", colorprefix, namestr, colorstr, ")^7"), msgin);
133                                 msgstr = strcat("\{1}\{13}^4* ", "^7", msgin);
134                         }
135                         else
136                                 msgstr = strcat("\{1}\{13}", colorstr, "(", colorprefix, namestr, colorstr, ") ^7", msgin);
137                         cmsgstr = strcat(colorstr, "(", colorprefix, namestr, colorstr, ")\n^7", msgin);
138                 }
139                 else
140                 {
141                         if(found_me)
142                         {
143                                 //msgin = strreplace("/me", "", msgin);
144                                 //msgin = substring(msgin, 3, strlen(msgin));
145                                 //msgin = strreplace("/me", strcat(colorprefix, namestr), msgin);
146                                 msgstr = strcat("\{1}^4* ^7", msgin);
147                         }
148                         else {
149                                 msgstr = "\{1}";
150                                 msgstr = strcat(msgstr, (namestr != "") ? strcat(colorprefix, namestr, "^7: ") : "^7");
151                                 msgstr = strcat(msgstr, msgin);
152                         }
153                         cmsgstr = "";
154                 }
155                 msgstr = strcat(strreplace("\n", " ", msgstr), "\n"); // newlines only are good for centerprint
156         }
157
158         string fullmsgstr = msgstr;
159         string fullcmsgstr = cmsgstr;
160
161         // FLOOD CONTROL
162         int flood = 0;
163         var .float flood_field = floodcontrol_chat;
164         if(floodcontrol && source)
165         {
166                 float flood_spl, flood_burst, flood_lmax;
167                 if(privatesay)
168                 {
169                         flood_spl = autocvar_g_chat_flood_spl_tell;
170                         flood_burst = autocvar_g_chat_flood_burst_tell;
171                         flood_lmax = autocvar_g_chat_flood_lmax_tell;
172                         flood_field = floodcontrol_chattell;
173                 }
174                 else if(teamsay)
175                 {
176                         flood_spl = autocvar_g_chat_flood_spl_team;
177                         flood_burst = autocvar_g_chat_flood_burst_team;
178                         flood_lmax = autocvar_g_chat_flood_lmax_team;
179                         flood_field = floodcontrol_chatteam;
180                 }
181                 else
182                 {
183                         flood_spl = autocvar_g_chat_flood_spl;
184                         flood_burst = autocvar_g_chat_flood_burst;
185                         flood_lmax = autocvar_g_chat_flood_lmax;
186                         flood_field = floodcontrol_chat;
187                 }
188                 flood_burst = max(0, flood_burst - 1);
189                 // to match explanation in default.cfg, a value of 3 must allow three-line bursts and not four!
190
191                 // do flood control for the default line size
192                 if(msgstr != "")
193                 {
194                         getWrappedLine_remaining = msgstr;
195                         msgstr = "";
196                         int lines = 0;
197                         while(getWrappedLine_remaining && (!flood_lmax || lines <= flood_lmax))
198                         {
199                                 msgstr = strcat(msgstr, " ", getWrappedLineLen(82.4289758859709, strlennocol)); // perl averagewidth.pl < gfx/vera-sans.width
200                                 ++lines;
201                         }
202                         msgstr = substring(msgstr, 1, strlen(msgstr) - 1);
203
204                         if(getWrappedLine_remaining != "")
205                         {
206                                 msgstr = strcat(msgstr, "\n");
207                                 flood = 2;
208                         }
209
210                         if (time >= source.(flood_field))
211                         {
212                                 source.(flood_field) = max(time - flood_burst * flood_spl, source.(flood_field)) + lines * flood_spl;
213                         }
214                         else
215                         {
216                                 flood = 1;
217                                 msgstr = fullmsgstr;
218                         }
219                 }
220                 else
221                 {
222                         if (time >= source.(flood_field))
223                                 source.(flood_field) = max(time - flood_burst * flood_spl, source.(flood_field)) + flood_spl;
224                         else
225                                 flood = 1;
226                 }
227
228                 if (timeout_status == TIMEOUT_ACTIVE) // when game is paused, no flood protection
229                         source.(flood_field) = flood = 0;
230         }
231
232         string sourcemsgstr, sourcecmsgstr;
233         if(flood == 2) // cannot happen for empty msgstr
234         {
235                 if(autocvar_g_chat_flood_notify_flooder)
236                 {
237                         sourcemsgstr = strcat(msgstr, "\n^3CHAT FLOOD CONTROL: ^7message too long, trimmed\n");
238                         sourcecmsgstr = "";
239                 }
240                 else
241                 {
242                         sourcemsgstr = fullmsgstr;
243                         sourcecmsgstr = fullcmsgstr;
244                 }
245                 cmsgstr = "";
246         }
247         else
248         {
249                 sourcemsgstr = msgstr;
250                 sourcecmsgstr = cmsgstr;
251         }
252
253         if (!privatesay && source && !(IS_PLAYER(source) || INGAME(source)) && !game_stopped
254                 && (teamsay || CHAT_NOSPECTATORS()))
255         {
256                 teamsay = -1; // spectators
257         }
258
259         if(flood)
260                 LOG_INFO("NOTE: ", playername(source.netname, source.team, IS_PLAYER(source)), "^7 is flooding.");
261
262         // build sourcemsgstr by cutting off a prefix and replacing it by the other one
263         if(privatesay)
264                 sourcemsgstr = strcat(privatemsgprefix, substring(sourcemsgstr, privatemsgprefixlen, -1));
265
266         int ret;
267         if(source && CS(source).muted)
268         {
269                 // always fake the message
270                 ret = -1;
271         }
272         else if(flood == 1)
273         {
274                 if (autocvar_g_chat_flood_notify_flooder)
275                 {
276                         sprint(source, strcat("^3CHAT FLOOD CONTROL: ^7wait ^1", ftos(source.(flood_field) - time), "^3 seconds\n"));
277                         ret = 0;
278                 }
279                 else
280                         ret = -1;
281         }
282         else
283         {
284                 ret = 1;
285         }
286
287         if (privatesay && source && !(IS_PLAYER(source) || INGAME(source)) && !game_stopped
288                 && (IS_PLAYER(privatesay) || INGAME(privatesay)) && CHAT_NOSPECTATORS())
289         {
290                 ret = -1; // just hide the message completely
291         }
292
293         MUTATOR_CALLHOOK(ChatMessage, source, ret);
294         ret = M_ARGV(1, int);
295
296         string event_log_msg = "";
297
298         if(sourcemsgstr != "" && ret != 0)
299         {
300                 if(ret < 0) // faked message, because the player is muted
301                 {
302                         sprint(source, sourcemsgstr);
303                         if(sourcecmsgstr != "" && !privatesay)
304                                 centerprint(source, sourcecmsgstr);
305                 }
306                 else if(privatesay) // private message, between 2 people only
307                 {
308                         sprint(source, sourcemsgstr);
309                         if (!autocvar_g_chat_tellprivacy) { dedicated_print(msgstr); } // send to server console too if "tellprivacy" is disabled
310                         if(!MUTATOR_CALLHOOK(ChatMessageTo, privatesay, source))
311                         {
312                                 if(IS_REAL_CLIENT(source) && ignore_playerinlist(source, privatesay)) // check ignored players from personal chat log (from "ignore" command)
313                                         return -1; // no sending to this player, thank you very much
314
315                                 sprint(privatesay, msgstr);
316                                 if(cmsgstr != "")
317                                         centerprint(privatesay, cmsgstr);
318                         }
319                 }
320                 else if ( teamsay && CS(source).active_minigame )
321                 {
322                         sprint(source, sourcemsgstr);
323                         dedicated_print(msgstr); // send to server console too
324                         FOREACH_CLIENT(IS_REAL_CLIENT(it) && it != source && CS(it).active_minigame == CS(source).active_minigame && !MUTATOR_CALLHOOK(ChatMessageTo, it, source), {
325                                 if(IS_REAL_CLIENT(source) && ignore_playerinlist(source, it)) // check ignored players from personal chat log (from "ignore" command)
326                                         continue; // no sending to this player, thank you very much
327
328                                 sprint(it, msgstr);
329                         });
330                         event_log_msg = sprintf(":chat_minigame:%d:%s:%s", source.playerid, CS(source).active_minigame.netname, msgin);
331
332                 }
333                 else if(teamsay > 0) // team message, only sent to team mates
334                 {
335                         sprint(source, sourcemsgstr);
336                         dedicated_print(msgstr); // send to server console too
337                         if(sourcecmsgstr != "")
338                                 centerprint(source, sourcecmsgstr);
339                         FOREACH_CLIENT((IS_PLAYER(it) || INGAME(it)) && IS_REAL_CLIENT(it) && it != source && it.team == source.team && !MUTATOR_CALLHOOK(ChatMessageTo, it, source), {
340                                 if(IS_REAL_CLIENT(source) && ignore_playerinlist(source, it)) // check ignored players from personal chat log (from "ignore" command)
341                                         continue; // no sending to this player, thank you very much
342
343                                 sprint(it, msgstr);
344                                 if(cmsgstr != "")
345                                         centerprint(it, cmsgstr);
346                         });
347                         event_log_msg = sprintf(":chat_team:%d:%d:%s", source.playerid, source.team, strreplace("\n", " ", msgin));
348                 }
349                 else if(teamsay < 0) // spectator message, only sent to spectators
350                 {
351                         sprint(source, sourcemsgstr);
352                         dedicated_print(msgstr); // send to server console too
353                         FOREACH_CLIENT(!(IS_PLAYER(it) || INGAME(it)) && IS_REAL_CLIENT(it) && it != source && !MUTATOR_CALLHOOK(ChatMessageTo, it, source), {
354                                 if(IS_REAL_CLIENT(source) && ignore_playerinlist(source, it)) // check ignored players from personal chat log (from "ignore" command)
355                                         continue; // no sending to this player, thank you very much
356
357                                 sprint(it, msgstr);
358                         });
359                         event_log_msg = sprintf(":chat_spec:%d:%s", source.playerid, strreplace("\n", " ", msgin));
360                 }
361                 else
362                 {
363                         if (source) {
364                                 sprint(source, sourcemsgstr);
365                                 dedicated_print(msgstr); // send to server console too
366                                 MX_Say(strcat(playername(source.netname, source.team, IS_PLAYER(source)), "^7: ", msgin));
367                         }
368                         FOREACH_CLIENT(IS_REAL_CLIENT(it) && it != source && !MUTATOR_CALLHOOK(ChatMessageTo, it, source), {
369                                 if(IS_REAL_CLIENT(source) && ignore_playerinlist(source, it)) // check ignored players from personal chat log (from "ignore" command)
370                                         continue; // no sending to this player, thank you very much
371
372                                 sprint(it, msgstr);
373                         });
374                         event_log_msg = sprintf(":chat:%d:%s", source.playerid, strreplace("\n", " ", msgin));
375                 }
376         }
377
378         if (autocvar_sv_eventlog && (event_log_msg != "")) {
379                 GameLogEcho(event_log_msg);
380         }
381
382         return ret;
383 }
384
385 entity findnearest(vector point, bool checkitems, vector axismod)
386 {
387     vector dist;
388     int num_nearest = 0;
389
390     IL_EACH(((checkitems) ? g_items : g_locations), ((checkitems) ? (it.target == "###item###") : (it.classname == "target_location")),
391     {
392         if ((it.items == IT_KEY1 || it.items == IT_KEY2) && it.target == "###item###")
393             dist = it.oldorigin;
394         else
395             dist = it.origin;
396         dist = dist - point;
397         dist = dist.x * axismod.x * '1 0 0' + dist.y * axismod.y * '0 1 0' + dist.z * axismod.z * '0 0 1';
398         float len = vlen2(dist);
399
400         int l;
401         for (l = 0; l < num_nearest; ++l)
402         {
403             if (len < nearest_length[l])
404                 break;
405         }
406
407         // now i tells us where to insert at
408         //   INSERTION SORT! YOU'VE SEEN IT! RUN!
409         if (l < NUM_NEAREST_ENTITIES)
410         {
411             for (int j = NUM_NEAREST_ENTITIES - 1; j >= l; --j)
412             {
413                 nearest_length[j + 1] = nearest_length[j];
414                 nearest_entity[j + 1] = nearest_entity[j];
415             }
416             nearest_length[l] = len;
417             nearest_entity[l] = it;
418             if (num_nearest < NUM_NEAREST_ENTITIES)
419                 num_nearest = num_nearest + 1;
420         }
421     });
422
423     // now use the first one from our list that we can see
424     for (int j = 0; j < num_nearest; ++j)
425     {
426         traceline(point, nearest_entity[j].origin, true, NULL);
427         if (trace_fraction == 1)
428         {
429             if (j != 0)
430                 LOG_TRACEF("Nearest point (%s) is not visible, using a visible one.", nearest_entity[0].netname);
431             return nearest_entity[j];
432         }
433     }
434
435     if (num_nearest == 0)
436         return NULL;
437
438     LOG_TRACE("Not seeing any location point, using nearest as fallback.");
439     /* DEBUGGING CODE:
440     dprint("Candidates were: ");
441     for(j = 0; j < num_nearest; ++j)
442     {
443         if(j != 0)
444                 dprint(", ");
445         dprint(nearest_entity[j].netname);
446     }
447     dprint("\n");
448     */
449
450     return nearest_entity[0];
451 }
452
453 string NearestLocation(vector p)
454 {
455     string ret = "somewhere";
456     entity loc = findnearest(p, false, '1 1 1');
457     if (loc)
458         ret = loc.message;
459     else
460     {
461         loc = findnearest(p, true, '1 1 4');
462         if (loc)
463             ret = loc.netname;
464     }
465     return ret;
466 }
467
468 string PlayerHealth(entity this)
469 {
470         float myhealth = floor(GetResource(this, RES_HEALTH));
471         if(myhealth == -666)
472                 return "spectating";
473         else if(myhealth == -2342 || (myhealth == 2342 && mapvote_initialized))
474                 return "observing";
475         else if(myhealth <= 0 || IS_DEAD(this))
476                 return "dead";
477         return ftos(myhealth);
478 }
479
480 string WeaponNameFromWeaponentity(entity this, .entity weaponentity)
481 {
482         entity wepent = this.(weaponentity);
483         if(!wepent)
484                 return "none";
485         else if(wepent.m_weapon != WEP_Null)
486                 return wepent.m_weapon.m_name;
487         else if(wepent.m_switchweapon != WEP_Null)
488                 return wepent.m_switchweapon.m_name;
489         return "none"; //REGISTRY_GET(Weapons, wepent.cnt).m_name;
490 }
491
492 string formatmessage(entity this, string msg)
493 {
494         float p, p1, p2;
495         float n;
496         vector cursor = '0 0 0';
497         entity cursor_ent = NULL;
498         string escape;
499         string replacement;
500         p = 0;
501         n = 7;
502         bool traced = false;
503
504         MUTATOR_CALLHOOK(PreFormatMessage, this, msg);
505         msg = M_ARGV(1, string);
506
507         while (1) {
508                 if (n < 1)
509                         break; // too many replacements
510
511                 n = n - 1;
512                 p1 = strstrofs(msg, "%", p); // NOTE: this destroys msg as it's a tempstring!
513                 p2 = strstrofs(msg, "\\", p); // NOTE: this destroys msg as it's a tempstring!
514
515                 if (p1 < 0)
516                         p1 = p2;
517
518                 if (p2 < 0)
519                         p2 = p1;
520
521                 p = min(p1, p2);
522
523                 if (p < 0)
524                         break;
525
526                 if(!traced)
527                 {
528                         WarpZone_crosshair_trace_plusvisibletriggers(this);
529                         cursor = trace_endpos;
530                         cursor_ent = trace_ent;
531                         traced = true;
532                 }
533
534                 replacement = substring(msg, p, 2);
535                 escape = substring(msg, p + 1, 1);
536
537                 .entity weaponentity = weaponentities[0]; // TODO: unhardcode
538
539                 switch(escape)
540                 {
541                         case "%": replacement = "%"; break;
542                         case "\\":replacement = "\\"; break;
543                         case "n": replacement = "\n"; break;
544                         case "a": replacement = ftos(floor(GetResource(this, RES_ARMOR))); break;
545                         case "h": replacement = PlayerHealth(this); break;
546                         case "l": replacement = NearestLocation(this.origin); break;
547                         case "y": replacement = NearestLocation(cursor); break;
548                         case "d": replacement = NearestLocation(this.death_origin); break;
549                         case "o": replacement = vtos(this.origin); break;
550                         case "O": replacement = sprintf("'%f %f %f'", this.origin.x, this.origin.y, this.origin.z); break;
551                         case "w": replacement = WeaponNameFromWeaponentity(this, weaponentity); break;
552                         case "W": replacement = GetAmmoName(this.(weaponentity).m_weapon.ammo_type); break;
553                         case "x": replacement = ((cursor_ent.netname == "" || !cursor_ent) ? "nothing" : cursor_ent.netname); break;
554                         case "s": replacement = ftos(vlen(this.velocity - this.velocity_z * '0 0 1')); break;
555                         case "S": replacement = ftos(vlen(this.velocity)); break;
556                         case "t": replacement = seconds_tostring(ceil(max(0, autocvar_timelimit * 60 + game_starttime - time))); break;
557                         case "T": replacement = seconds_tostring(floor(time - game_starttime)); break;
558                         default:
559                         {
560                                 MUTATOR_CALLHOOK(FormatMessage, this, escape, replacement, msg);
561                                 replacement = M_ARGV(2, string);
562                                 break;
563                         }
564                 }
565
566                 msg = strcat(substring(msg, 0, p), replacement, substring(msg, p+2, strlen(msg) - (p+2)));
567                 p = p + strlen(replacement);
568         }
569         return msg;
570 }
571
572 ERASEABLE
573 void PrintToChat(entity client, string text)
574 {
575         text = strcat("\{1}^7", text, "\n");
576         sprint(client, text);
577 }
578
579 ERASEABLE
580 void DebugPrintToChat(entity client, string text)
581 {
582         if (autocvar_developer > 0)
583         {
584                 PrintToChat(client, text);
585         }
586 }
587
588 ERASEABLE
589 void PrintToChatAll(string text)
590 {
591         text = strcat("\{1}^7", text, "\n");
592         bprint(text);
593 }
594
595 ERASEABLE
596 void DebugPrintToChatAll(string text)
597 {
598         if (autocvar_developer > 0)
599         {
600                 PrintToChatAll(text);
601         }
602 }
603
604 ERASEABLE
605 void PrintToChatTeam(int team_num, string text)
606 {
607         text = strcat("\{1}^7", text, "\n");
608         FOREACH_CLIENT(IS_REAL_CLIENT(it),
609         {
610                 if (it.team == team_num)
611                 {
612                         sprint(it, text);
613                 }
614         });
615 }
616
617 ERASEABLE
618 void DebugPrintToChatTeam(int team_num, string text)
619 {
620         if (autocvar_developer > 0)
621         {
622                 PrintToChatTeam(team_num, text);
623         }
624 }