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