]> 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) || INGAME(source)))
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                 // z411 : Why?
210                 //if (timeout_status == TIMEOUT_ACTIVE) // when game is paused, no flood protection
211                 //      source.(flood_field) = flood = 0;
212         }
213
214         string sourcemsgstr, sourcecmsgstr;
215         if(flood == 2) // cannot happen for empty msgstr
216         {
217                 if(autocvar_g_chat_flood_notify_flooder)
218                 {
219                         sourcemsgstr = strcat(msgstr, "\n^3FLOOD CONTROL: ^7message too long, trimmed\n");
220                         sourcecmsgstr = "";
221                 }
222                 else
223                 {
224                         sourcemsgstr = fullmsgstr;
225                         sourcecmsgstr = fullcmsgstr;
226                 }
227                 cmsgstr = "";
228         }
229         else
230         {
231                 sourcemsgstr = msgstr;
232                 sourcecmsgstr = cmsgstr;
233         }
234
235         if (!privatesay && source && !(IS_PLAYER(source) || INGAME(source)) && !game_stopped
236                 && (teamsay || CHAT_NOSPECTATORS()))
237         {
238                 teamsay = -1; // spectators
239         }
240
241         if(flood)
242                 LOG_INFO("NOTE: ", playername(source.netname, source.team, IS_PLAYER(source)), "^7 is flooding.");
243
244         // build sourcemsgstr by cutting off a prefix and replacing it by the other one
245         if(privatesay)
246                 sourcemsgstr = strcat(privatemsgprefix, substring(sourcemsgstr, privatemsgprefixlen, -1));
247
248         int ret;
249         if(source && CS(source).muted)
250         {
251                 // always fake the message
252                 ret = -1;
253         }
254         else if(flood == 1)
255         {
256                 if (autocvar_g_chat_flood_notify_flooder)
257                 {
258                         sprint(source, strcat("^3FLOOD CONTROL: ^7wait ^1", ftos(source.(flood_field) - time), "^3 seconds\n"));
259                         ret = 0;
260                 }
261                 else
262                         ret = -1;
263         }
264         else
265         {
266                 ret = 1;
267         }
268
269         if (privatesay && source && !(IS_PLAYER(source) || INGAME(source)) && !game_stopped
270                 && (IS_PLAYER(privatesay) || INGAME(privatesay)) && CHAT_NOSPECTATORS())
271         {
272                 ret = -1; // just hide the message completely
273         }
274
275         MUTATOR_CALLHOOK(ChatMessage, source, ret);
276         ret = M_ARGV(1, int);
277
278         string event_log_msg = "";
279
280         if(sourcemsgstr != "" && ret != 0)
281         {
282                 if(ret < 0) // faked message, because the player is muted
283                 {
284                         sprint(source, sourcemsgstr);
285                         if(sourcecmsgstr != "" && !privatesay)
286                                 centerprint(source, sourcecmsgstr);
287                 }
288                 else if(privatesay) // private message, between 2 people only
289                 {
290                         sprint(source, sourcemsgstr);
291                         if (!autocvar_g_chat_tellprivacy) { dedicated_print(msgstr); } // send to server console too if "tellprivacy" is disabled
292                         if(!MUTATOR_CALLHOOK(ChatMessageTo, privatesay, source))
293                         {
294                                 sprint(privatesay, msgstr);
295                                 if(cmsgstr != "")
296                                         centerprint(privatesay, cmsgstr);
297                         }
298                 }
299                 else if ( teamsay && CS(source).active_minigame )
300                 {
301                         sprint(source, sourcemsgstr);
302                         dedicated_print(msgstr); // send to server console too
303                         FOREACH_CLIENT(IS_REAL_CLIENT(it) && it != source && CS(it).active_minigame == CS(source).active_minigame && !MUTATOR_CALLHOOK(ChatMessageTo, it, source), {
304                                 sprint(it, msgstr);
305                         });
306                         event_log_msg = sprintf(":chat_minigame:%d:%s:%s", source.playerid, CS(source).active_minigame.netname, msgin);
307
308                 }
309                 else if(teamsay > 0) // team message, only sent to team mates
310                 {
311                         sprint(source, sourcemsgstr);
312                         dedicated_print(msgstr); // send to server console too
313                         if(sourcecmsgstr != "")
314                                 centerprint(source, sourcecmsgstr);
315                         FOREACH_CLIENT((IS_PLAYER(it) || INGAME(it)) && IS_REAL_CLIENT(it) && it != source && it.team == source.team && !MUTATOR_CALLHOOK(ChatMessageTo, it, source), {
316                                 sprint(it, msgstr);
317                                 if(cmsgstr != "")
318                                         centerprint(it, cmsgstr);
319                         });
320                         event_log_msg = sprintf(":chat_team:%d:%d:%s", source.playerid, source.team, strreplace("\n", " ", msgin));
321                 }
322                 else if(teamsay < 0) // spectator message, only sent to spectators
323                 {
324                         sprint(source, sourcemsgstr);
325                         dedicated_print(msgstr); // send to server console too
326                         FOREACH_CLIENT(!(IS_PLAYER(it) || INGAME(it)) && IS_REAL_CLIENT(it) && it != source && !MUTATOR_CALLHOOK(ChatMessageTo, it, source), {
327                                 sprint(it, msgstr);
328                         });
329                         
330                         if(!play_chatsound(source, msgin))
331                                 event_log_msg = sprintf(":chat_spec:%d:%s", source.playerid, strreplace("\n", " ", msgin));
332                 }
333                 else
334                 {
335                         if (source) {
336                                 sprint(source, sourcemsgstr);
337                                 dedicated_print(msgstr); // send to server console too
338                                 MX_Say(strcat(playername(source.netname, source.team, IS_PLAYER(source)), "^7: ", msgin));
339                         }
340                         FOREACH_CLIENT(IS_REAL_CLIENT(it) && it != source && !MUTATOR_CALLHOOK(ChatMessageTo, it, source), {
341                                 sprint(it, msgstr);
342                         });
343                         
344                         if(!play_chatsound(source, msgin))
345                                 event_log_msg = sprintf(":chat:%d:%s", source.playerid, strreplace("\n", " ", msgin));
346                 }
347         }
348
349         if (autocvar_sv_eventlog && (event_log_msg != "")) {
350                 GameLogEcho(event_log_msg);
351         }
352
353         return ret;
354 }
355
356 bool play_chatsound(entity source, string msgin)
357 {
358         if(autocvar_sv_chat_sounds && CS_CVAR(source).cvar_cl_chat_sounds) {
359                 var .float flood_sound = floodcontrol_chatsound;
360                 
361                 if (source.(flood_sound) < time - autocvar_sv_chat_sounds_flood) {
362                         string rawmsg;
363                         bool found = false;
364                         rawmsg = strreplace("\n", " ", msgin);
365                         
366                         FOREACH_WORD(autocvar_sv_chat_sounds_list, it == rawmsg, { found = true; });
367                         if (found) {
368                                 FOREACH_CLIENT(IS_REAL_CLIENT(it) && CS_CVAR(it).cvar_cl_chat_sounds, {
369                                         msg_entity = it;
370                                         WriteHeader(MSG_ONE, TE_CSQC_CHATSOUND);
371                                         WriteString(MSG_ONE, rawmsg);
372                                 });
373                                 source.(flood_sound) = time;
374                                 return true;
375                         }
376                 }
377         }
378         
379         return false;
380 }
381
382 entity findnearest(vector point, bool checkitems, vector axismod)
383 {
384     vector dist;
385     int num_nearest = 0;
386
387     IL_EACH(((checkitems) ? g_items : g_locations), ((checkitems) ? (it.target == "###item###") : (it.classname == "target_location")),
388     {
389         if ((it.items == IT_KEY1 || it.items == IT_KEY2) && it.target == "###item###")
390             dist = it.oldorigin;
391         else
392             dist = it.origin;
393         dist = dist - point;
394         dist = dist.x * axismod.x * '1 0 0' + dist.y * axismod.y * '0 1 0' + dist.z * axismod.z * '0 0 1';
395         float len = vlen2(dist);
396
397         int l;
398         for (l = 0; l < num_nearest; ++l)
399         {
400             if (len < nearest_length[l])
401                 break;
402         }
403
404         // now i tells us where to insert at
405         //   INSERTION SORT! YOU'VE SEEN IT! RUN!
406         if (l < NUM_NEAREST_ENTITIES)
407         {
408             for (int j = NUM_NEAREST_ENTITIES - 1; j >= l; --j)
409             {
410                 nearest_length[j + 1] = nearest_length[j];
411                 nearest_entity[j + 1] = nearest_entity[j];
412             }
413             nearest_length[l] = len;
414             nearest_entity[l] = it;
415             if (num_nearest < NUM_NEAREST_ENTITIES)
416                 num_nearest = num_nearest + 1;
417         }
418     });
419
420     // now use the first one from our list that we can see
421     for (int j = 0; j < num_nearest; ++j)
422     {
423         traceline(point, nearest_entity[j].origin, true, NULL);
424         if (trace_fraction == 1)
425         {
426             if (j != 0)
427                 LOG_TRACEF("Nearest point (%s) is not visible, using a visible one.", nearest_entity[0].netname);
428             return nearest_entity[j];
429         }
430     }
431
432     if (num_nearest == 0)
433         return NULL;
434
435     LOG_TRACE("Not seeing any location point, using nearest as fallback.");
436     /* DEBUGGING CODE:
437     dprint("Candidates were: ");
438     for(j = 0; j < num_nearest; ++j)
439     {
440         if(j != 0)
441                 dprint(", ");
442         dprint(nearest_entity[j].netname);
443     }
444     dprint("\n");
445     */
446
447     return nearest_entity[0];
448 }
449
450 string NearestLocation(vector p)
451 {
452     string ret = "somewhere";
453     entity loc = findnearest(p, false, '1 1 1');
454     if (loc)
455         ret = loc.message;
456     else
457     {
458         loc = findnearest(p, true, '1 1 4');
459         if (loc)
460             ret = loc.netname;
461     }
462     return ret;
463 }
464
465 string PlayerHealth(entity this)
466 {
467         float myhealth = floor(GetResource(this, RES_HEALTH));
468         if(myhealth == -666)
469                 return "spectating";
470         else if(myhealth == -2342 || (myhealth == 2342 && mapvote_initialized))
471                 return "observing";
472         else if(myhealth <= 0 || IS_DEAD(this))
473                 return "dead";
474         return ftos(myhealth);
475 }
476
477 string WeaponNameFromWeaponentity(entity this, .entity weaponentity)
478 {
479         entity wepent = this.(weaponentity);
480         if(!wepent)
481                 return "none";
482         else if(wepent.m_weapon != WEP_Null)
483                 return wepent.m_weapon.m_name;
484         else if(wepent.m_switchweapon != WEP_Null)
485                 return wepent.m_switchweapon.m_name;
486         return "none"; //REGISTRY_GET(Weapons, wepent.cnt).m_name;
487 }
488
489 string formatmessage(entity this, string msg)
490 {
491         float p, p1, p2;
492         float n;
493         vector cursor = '0 0 0';
494         entity cursor_ent = NULL;
495         string escape;
496         string replacement;
497         p = 0;
498         n = 7;
499         bool traced = false;
500
501         MUTATOR_CALLHOOK(PreFormatMessage, this, msg);
502         msg = M_ARGV(1, string);
503
504         while (1) {
505                 if (n < 1)
506                         break; // too many replacements
507
508                 n = n - 1;
509                 p1 = strstrofs(msg, "%", p); // NOTE: this destroys msg as it's a tempstring!
510                 p2 = strstrofs(msg, "\\", p); // NOTE: this destroys msg as it's a tempstring!
511
512                 if (p1 < 0)
513                         p1 = p2;
514
515                 if (p2 < 0)
516                         p2 = p1;
517
518                 p = min(p1, p2);
519
520                 if (p < 0)
521                         break;
522
523                 if(!traced)
524                 {
525                         WarpZone_crosshair_trace_plusvisibletriggers(this);
526                         cursor = trace_endpos;
527                         cursor_ent = trace_ent;
528                         traced = true;
529                 }
530
531                 replacement = substring(msg, p, 2);
532                 escape = substring(msg, p + 1, 1);
533
534                 .entity weaponentity = weaponentities[0]; // TODO: unhardcode
535
536                 switch(escape)
537                 {
538                         case "%": replacement = "%"; break;
539                         case "\\":replacement = "\\"; break;
540                         case "n": replacement = "\n"; break;
541                         case "a": replacement = ftos(floor(GetResource(this, RES_ARMOR))); break;
542                         case "h": replacement = PlayerHealth(this); break;
543                         case "l": replacement = NearestLocation(this.origin); break;
544                         case "y": replacement = NearestLocation(cursor); break;
545                         case "d": replacement = NearestLocation(this.death_origin); break;
546                         case "w": replacement = WeaponNameFromWeaponentity(this, weaponentity); break;
547                         case "W": replacement = GetAmmoName(this.(weaponentity).m_weapon.ammo_type); break;
548                         case "x": replacement = ((cursor_ent.netname == "" || !cursor_ent) ? "nothing" : cursor_ent.netname); break;
549                         case "s": replacement = ftos(vlen(this.velocity - this.velocity_z * '0 0 1')); break;
550                         case "S": replacement = ftos(vlen(this.velocity)); break;
551                         case "t": replacement = seconds_tostring(ceil(max(0, autocvar_timelimit * 60 + game_starttime - time))); break;
552                         case "T": replacement = seconds_tostring(floor(time - game_starttime)); break;
553                         default:
554                         {
555                                 MUTATOR_CALLHOOK(FormatMessage, this, escape, replacement, msg);
556                                 replacement = M_ARGV(2, string);
557                                 break;
558                         }
559                 }
560
561                 msg = strcat(substring(msg, 0, p), replacement, substring(msg, p+2, strlen(msg) - (p+2)));
562                 p = p + strlen(replacement);
563         }
564         return msg;
565 }
566
567 ERASEABLE
568 void PrintToChat(entity client, string text)
569 {
570         text = strcat("\{1}^7", text, "\n");
571         sprint(client, text);
572 }
573
574 ERASEABLE
575 void DebugPrintToChat(entity client, string text)
576 {
577         if (autocvar_developer > 0)
578         {
579                 PrintToChat(client, text);
580         }
581 }
582
583 ERASEABLE
584 void PrintToChatAll(string text)
585 {
586         text = strcat("\{1}^7", text, "\n");
587         bprint(text);
588 }
589
590 ERASEABLE
591 void DebugPrintToChatAll(string text)
592 {
593         if (autocvar_developer > 0)
594         {
595                 PrintToChatAll(text);
596         }
597 }
598
599 ERASEABLE
600 void PrintToChatTeam(int team_num, string text)
601 {
602         text = strcat("\{1}^7", text, "\n");
603         FOREACH_CLIENT(IS_REAL_CLIENT(it),
604         {
605                 if (it.team == team_num)
606                 {
607                         sprint(it, text);
608                 }
609         });
610 }
611
612 ERASEABLE
613 void DebugPrintToChatTeam(int team_num, string text)
614 {
615         if (autocvar_developer > 0)
616         {
617                 PrintToChatTeam(team_num, text);
618         }
619 }