3 #include <common/gamemodes/_mod.qh>
4 #include <common/mapobjects/target/location.qh>
5 #include <common/mapobjects/triggers.qh>
6 #include <common/notifications/all.qh>
7 #include <common/teams.qh>
8 #include <common/util.qh>
9 #include <common/weapons/weapon.qh>
10 #include <common/wepent.qh>
11 #include <server/command/cmd.qh>
12 #include <server/command/common.qh>
13 #include <server/gamelog.qh>
14 #include <server/main.qh>
15 #include <server/mapvoting.qh>
16 #include <server/mutators/_mod.qh>
17 #include <server/weapons/tracing.qh>
18 #include <server/world.qh>
21 * message "": do not say, just test flood control
27 int Say(entity source, int teamsay, entity privatesay, string msgin, bool floodcontrol)
29 if(!autocvar_g_chat_allowed && IS_REAL_CLIENT(source))
31 Send_Notification(NOTIF_ONE_ONLY, source, MSG_INFO, INFO_CHAT_DISABLED);
35 if(!autocvar_g_chat_private_allowed && privatesay)
37 Send_Notification(NOTIF_ONE_ONLY, source, MSG_INFO, INFO_CHAT_PRIVATE_DISABLED);
41 if(!autocvar_g_chat_spectator_allowed && IS_OBSERVER(source))
43 Send_Notification(NOTIF_ONE_ONLY, source, MSG_INFO, INFO_CHAT_SPECTATOR_DISABLED);
47 if(!autocvar_g_chat_team_allowed && teamsay)
49 Send_Notification(NOTIF_ONE_ONLY, source, MSG_INFO, INFO_CHAT_TEAM_DISABLED);
53 if (!teamsay && !privatesay && substring(msgin, 0, 1) == " ")
54 msgin = substring(msgin, 1, -1); // work around DP say bug (say_team does not have this!)
57 msgin = formatmessage(source, msgin);
60 if (!(IS_PLAYER(source) || INGAME(source)))
61 colorstr = "^0"; // black for spectators
63 colorstr = Team_ColorCode(source.team);
76 msgin = trigger_magicear_processmessage_forallears(source, teamsay, privatesay, msgin);
79 * using bprint solves this... me stupid
80 // how can we prevent the message from appearing in a listen server?
81 // for now, just give "say" back and only handle say_team
84 clientcommand(source, strcat("say ", msgin));
91 namestr = playername(source.netname, source.team, (autocvar_g_chat_teamcolors && IS_PLAYER(source)));
93 string colorprefix = (strdecolorize(namestr) == namestr) ? "^3" : "^7";
95 string msgstr = "", cmsgstr = "";
96 string privatemsgprefix = string_null;
97 int privatemsgprefixlen = 0;
100 bool found_me = false;
101 if(strstrofs(msgin, "/me", 0) >= 0)
103 string newmsgin = "";
104 string newnamestr = ((teamsay) ? strcat(colorstr, "(", colorprefix, namestr, colorstr, ")", "^7") : strcat(colorprefix, namestr, "^7"));
105 FOREACH_WORD(msgin, true,
107 if(strdecolorize(it) == "/me")
110 newmsgin = cons(newmsgin, newnamestr);
113 newmsgin = cons(newmsgin, it);
120 msgstr = strcat("\{1}\{13}* ", colorprefix, namestr, "^3 tells you: ^7");
121 privatemsgprefixlen = strlen(msgstr);
122 msgstr = strcat(msgstr, msgin);
123 cmsgstr = strcat(colorstr, colorprefix, namestr, "^3 tells you:\n^7", msgin);
124 privatemsgprefix = strcat("\{1}\{13}* ^3You tell ", playername(privatesay.netname, privatesay.team, (autocvar_g_chat_teamcolors && IS_PLAYER(privatesay))), ": ^7");
130 //msgin = strreplace("/me", "", msgin);
131 //msgin = substring(msgin, 3, strlen(msgin));
132 //msgin = strreplace("/me", strcat(colorstr, "(", colorprefix, namestr, colorstr, ")^7"), msgin);
133 msgstr = strcat("\{1}\{13}^4* ", "^7", msgin);
136 msgstr = strcat("\{1}\{13}", colorstr, "(", colorprefix, namestr, colorstr, ") ^7", msgin);
137 cmsgstr = strcat(colorstr, "(", colorprefix, namestr, colorstr, ")\n^7", msgin);
143 //msgin = strreplace("/me", "", msgin);
144 //msgin = substring(msgin, 3, strlen(msgin));
145 //msgin = strreplace("/me", strcat(colorprefix, namestr), msgin);
146 msgstr = strcat("\{1}^4* ^7", msgin);
150 msgstr = strcat(msgstr, (namestr != "") ? strcat(colorprefix, namestr, "^7: ") : "^7");
151 msgstr = strcat(msgstr, msgin);
155 msgstr = strcat(strreplace("\n", " ", msgstr), "\n"); // newlines only are good for centerprint
158 string fullmsgstr = msgstr;
159 string fullcmsgstr = cmsgstr;
163 var .float flood_field = floodcontrol_chat;
164 if(floodcontrol && source)
166 float flood_spl, flood_burst, flood_lmax;
169 flood_spl = autocvar_g_chat_flood_spl_tell;
170 flood_burst = autocvar_g_chat_flood_burst_tell;
171 flood_lmax = autocvar_g_chat_flood_lmax_tell;
172 flood_field = floodcontrol_chattell;
176 flood_spl = autocvar_g_chat_flood_spl_team;
177 flood_burst = autocvar_g_chat_flood_burst_team;
178 flood_lmax = autocvar_g_chat_flood_lmax_team;
179 flood_field = floodcontrol_chatteam;
183 flood_spl = autocvar_g_chat_flood_spl;
184 flood_burst = autocvar_g_chat_flood_burst;
185 flood_lmax = autocvar_g_chat_flood_lmax;
186 flood_field = floodcontrol_chat;
188 flood_burst = max(0, flood_burst - 1);
189 // to match explanation in default.cfg, a value of 3 must allow three-line bursts and not four!
191 // do flood control for the default line size
194 getWrappedLine_remaining = msgstr;
197 while(getWrappedLine_remaining && (!flood_lmax || lines <= flood_lmax))
199 msgstr = strcat(msgstr, " ", getWrappedLineLen(82.4289758859709, strlennocol)); // perl averagewidth.pl < gfx/vera-sans.width
202 msgstr = substring(msgstr, 1, strlen(msgstr) - 1);
204 if(getWrappedLine_remaining != "")
206 msgstr = strcat(msgstr, "\n");
210 if (time >= source.(flood_field))
212 source.(flood_field) = max(time - flood_burst * flood_spl, source.(flood_field)) + lines * flood_spl;
222 if (time >= source.(flood_field))
223 source.(flood_field) = max(time - flood_burst * flood_spl, source.(flood_field)) + flood_spl;
228 if (timeout_status == TIMEOUT_ACTIVE) // when game is paused, no flood protection
229 source.(flood_field) = flood = 0;
232 string sourcemsgstr, sourcecmsgstr;
233 if(flood == 2) // cannot happen for empty msgstr
235 if(autocvar_g_chat_flood_notify_flooder)
237 sourcemsgstr = strcat(msgstr, "\n^3CHAT FLOOD CONTROL: ^7message too long, trimmed\n");
242 sourcemsgstr = fullmsgstr;
243 sourcecmsgstr = fullcmsgstr;
249 sourcemsgstr = msgstr;
250 sourcecmsgstr = cmsgstr;
253 if (!privatesay && source && !(IS_PLAYER(source) || INGAME(source)) && !game_stopped
254 && (teamsay || CHAT_NOSPECTATORS()))
256 teamsay = -1; // spectators
260 LOG_INFO("NOTE: ", playername(source.netname, source.team, IS_PLAYER(source)), "^7 is flooding.");
262 // build sourcemsgstr by cutting off a prefix and replacing it by the other one
264 sourcemsgstr = strcat(privatemsgprefix, substring(sourcemsgstr, privatemsgprefixlen, -1));
267 if(source && CS(source).muted)
269 // always fake the message
274 if (autocvar_g_chat_flood_notify_flooder)
276 sprint(source, strcat("^3CHAT FLOOD CONTROL: ^7wait ^1", ftos(source.(flood_field) - time), "^3 seconds\n"));
287 if (privatesay && source && !(IS_PLAYER(source) || INGAME(source)) && !game_stopped
288 && (IS_PLAYER(privatesay) || INGAME(privatesay)) && CHAT_NOSPECTATORS())
290 ret = -1; // just hide the message completely
293 MUTATOR_CALLHOOK(ChatMessage, source, ret);
294 ret = M_ARGV(1, int);
296 string event_log_msg = "";
298 if(sourcemsgstr != "" && ret != 0)
300 if(ret < 0) // faked message, because the player is muted
302 sprint(source, sourcemsgstr);
303 if(sourcecmsgstr != "" && !privatesay)
304 centerprint(source, sourcecmsgstr);
306 else if(privatesay) // private message, between 2 people only
308 sprint(source, sourcemsgstr);
309 if (!autocvar_g_chat_tellprivacy) { dedicated_print(msgstr); } // send to server console too if "tellprivacy" is disabled
310 if(!MUTATOR_CALLHOOK(ChatMessageTo, privatesay, source))
312 if(IS_REAL_CLIENT(source) && ignore_playerinlist(source, privatesay)) // check ignored players from personal chat log (from "ignore" command)
313 return -1; // no sending to this player, thank you very much
315 sprint(privatesay, msgstr);
317 centerprint(privatesay, cmsgstr);
320 else if ( teamsay && CS(source).active_minigame )
322 sprint(source, sourcemsgstr);
323 dedicated_print(msgstr); // send to server console too
324 FOREACH_CLIENT(IS_REAL_CLIENT(it) && it != source && CS(it).active_minigame == CS(source).active_minigame && !MUTATOR_CALLHOOK(ChatMessageTo, it, source), {
325 if(IS_REAL_CLIENT(source) && ignore_playerinlist(source, it)) // check ignored players from personal chat log (from "ignore" command)
326 continue; // no sending to this player, thank you very much
330 event_log_msg = sprintf(":chat_minigame:%d:%s:%s", source.playerid, CS(source).active_minigame.netname, msgin);
333 else if(teamsay > 0) // team message, only sent to team mates
335 sprint(source, sourcemsgstr);
336 dedicated_print(msgstr); // send to server console too
337 if(sourcecmsgstr != "")
338 centerprint(source, sourcecmsgstr);
339 FOREACH_CLIENT((IS_PLAYER(it) || INGAME(it)) && IS_REAL_CLIENT(it) && it != source && it.team == source.team && !MUTATOR_CALLHOOK(ChatMessageTo, it, source), {
340 if(IS_REAL_CLIENT(source) && ignore_playerinlist(source, it)) // check ignored players from personal chat log (from "ignore" command)
341 continue; // no sending to this player, thank you very much
345 centerprint(it, cmsgstr);
347 event_log_msg = sprintf(":chat_team:%d:%d:%s", source.playerid, source.team, strreplace("\n", " ", msgin));
349 else if(teamsay < 0) // spectator message, only sent to spectators
351 sprint(source, sourcemsgstr);
352 dedicated_print(msgstr); // send to server console too
353 FOREACH_CLIENT(!(IS_PLAYER(it) || INGAME(it)) && IS_REAL_CLIENT(it) && it != source && !MUTATOR_CALLHOOK(ChatMessageTo, it, source), {
354 if(IS_REAL_CLIENT(source) && ignore_playerinlist(source, it)) // check ignored players from personal chat log (from "ignore" command)
355 continue; // no sending to this player, thank you very much
359 event_log_msg = sprintf(":chat_spec:%d:%s", source.playerid, strreplace("\n", " ", msgin));
364 sprint(source, sourcemsgstr);
365 dedicated_print(msgstr); // send to server console too
366 MX_Say(strcat(playername(source.netname, source.team, IS_PLAYER(source)), "^7: ", msgin));
368 FOREACH_CLIENT(IS_REAL_CLIENT(it) && it != source && !MUTATOR_CALLHOOK(ChatMessageTo, it, source), {
369 if(IS_REAL_CLIENT(source) && ignore_playerinlist(source, it)) // check ignored players from personal chat log (from "ignore" command)
370 continue; // no sending to this player, thank you very much
374 event_log_msg = sprintf(":chat:%d:%s", source.playerid, strreplace("\n", " ", msgin));
378 if (autocvar_sv_eventlog && (event_log_msg != "")) {
379 GameLogEcho(event_log_msg);
385 entity findnearest(vector point, bool checkitems, vector axismod)
390 IL_EACH(((checkitems) ? g_items : g_locations), ((checkitems) ? (it.target == "###item###") : (it.classname == "target_location")),
392 if ((it.items == IT_KEY1 || it.items == IT_KEY2) && it.target == "###item###")
397 dist = dist.x * axismod.x * '1 0 0' + dist.y * axismod.y * '0 1 0' + dist.z * axismod.z * '0 0 1';
398 float len = vlen2(dist);
401 for (l = 0; l < num_nearest; ++l)
403 if (len < nearest_length[l])
407 // now i tells us where to insert at
408 // INSERTION SORT! YOU'VE SEEN IT! RUN!
409 if (l < NUM_NEAREST_ENTITIES)
411 for (int j = NUM_NEAREST_ENTITIES - 1; j >= l; --j)
413 nearest_length[j + 1] = nearest_length[j];
414 nearest_entity[j + 1] = nearest_entity[j];
416 nearest_length[l] = len;
417 nearest_entity[l] = it;
418 if (num_nearest < NUM_NEAREST_ENTITIES)
419 num_nearest = num_nearest + 1;
423 // now use the first one from our list that we can see
424 for (int j = 0; j < num_nearest; ++j)
426 traceline(point, nearest_entity[j].origin, true, NULL);
427 if (trace_fraction == 1)
430 LOG_TRACEF("Nearest point (%s) is not visible, using a visible one.", nearest_entity[0].netname);
431 return nearest_entity[j];
435 if (num_nearest == 0)
438 LOG_TRACE("Not seeing any location point, using nearest as fallback.");
440 dprint("Candidates were: ");
441 for(j = 0; j < num_nearest; ++j)
445 dprint(nearest_entity[j].netname);
450 return nearest_entity[0];
453 string NearestLocation(vector p)
455 string ret = "somewhere";
456 entity loc = findnearest(p, false, '1 1 1');
461 loc = findnearest(p, true, '1 1 4');
468 string PlayerHealth(entity this)
470 float myhealth = floor(GetResource(this, RES_HEALTH));
473 else if(myhealth == -2342 || (myhealth == 2342 && mapvote_initialized))
475 else if(myhealth <= 0 || IS_DEAD(this))
477 return ftos(myhealth);
480 string WeaponNameFromWeaponentity(entity this, .entity weaponentity)
482 entity wepent = this.(weaponentity);
485 else if(wepent.m_weapon != WEP_Null)
486 return wepent.m_weapon.m_name;
487 else if(wepent.m_switchweapon != WEP_Null)
488 return wepent.m_switchweapon.m_name;
489 return "none"; //REGISTRY_GET(Weapons, wepent.cnt).m_name;
492 string formatmessage(entity this, string msg)
496 vector cursor = '0 0 0';
497 entity cursor_ent = NULL;
504 MUTATOR_CALLHOOK(PreFormatMessage, this, msg);
505 msg = M_ARGV(1, string);
509 break; // too many replacements
512 p1 = strstrofs(msg, "%", p); // NOTE: this destroys msg as it's a tempstring!
513 p2 = strstrofs(msg, "\\", p); // NOTE: this destroys msg as it's a tempstring!
528 WarpZone_crosshair_trace_plusvisibletriggers(this);
529 cursor = trace_endpos;
530 cursor_ent = trace_ent;
534 replacement = substring(msg, p, 2);
535 escape = substring(msg, p + 1, 1);
537 .entity weaponentity = weaponentities[0]; // TODO: unhardcode
541 case "%": replacement = "%"; break;
542 case "\\":replacement = "\\"; break;
543 case "n": replacement = "\n"; break;
544 case "a": replacement = ftos(floor(GetResource(this, RES_ARMOR))); break;
545 case "h": replacement = PlayerHealth(this); break;
546 case "l": replacement = NearestLocation(this.origin); break;
547 case "y": replacement = NearestLocation(cursor); break;
548 case "d": replacement = NearestLocation(this.death_origin); break;
549 case "o": replacement = vtos(this.origin); break;
550 case "O": replacement = sprintf("'%f %f %f'", this.origin.x, this.origin.y, this.origin.z); break;
551 case "w": replacement = WeaponNameFromWeaponentity(this, weaponentity); break;
552 case "W": replacement = GetAmmoName(this.(weaponentity).m_weapon.ammo_type); break;
553 case "x": replacement = ((cursor_ent.netname == "" || !cursor_ent) ? "nothing" : cursor_ent.netname); break;
554 case "s": replacement = ftos(vlen(this.velocity - this.velocity_z * '0 0 1')); break;
555 case "S": replacement = ftos(vlen(this.velocity)); break;
556 case "t": replacement = seconds_tostring(ceil(max(0, autocvar_timelimit * 60 + game_starttime - time))); break;
557 case "T": replacement = seconds_tostring(floor(time - game_starttime)); break;
560 MUTATOR_CALLHOOK(FormatMessage, this, escape, replacement, msg);
561 replacement = M_ARGV(2, string);
566 msg = strcat(substring(msg, 0, p), replacement, substring(msg, p+2, strlen(msg) - (p+2)));
567 p = p + strlen(replacement);
573 void PrintToChat(entity client, string text)
575 text = strcat("\{1}^7", text, "\n");
576 sprint(client, text);
580 void DebugPrintToChat(entity client, string text)
582 if (autocvar_developer > 0)
584 PrintToChat(client, text);
589 void PrintToChatAll(string text)
591 text = strcat("\{1}^7", text, "\n");
596 void DebugPrintToChatAll(string text)
598 if (autocvar_developer > 0)
600 PrintToChatAll(text);
605 void PrintToChatTeam(int team_num, string text)
607 text = strcat("\{1}^7", text, "\n");
608 FOREACH_CLIENT(IS_REAL_CLIENT(it),
610 if (it.team == team_num)
618 void DebugPrintToChatTeam(int team_num, string text)
620 if (autocvar_developer > 0)
622 PrintToChatTeam(team_num, text);