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