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