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>
20 * message "": do not say, just test flood control
26 int Say(entity source, int teamsay, entity privatesay, string msgin, bool floodcontrol)
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!)
32 msgin = formatmessage(source, msgin);
35 if (!(IS_PLAYER(source) || source.caplayer))
36 colorstr = "^0"; // black for spectators
38 colorstr = Team_ColorCode(source.team);
51 msgin = trigger_magicear_processmessage_forallears(source, teamsay, privatesay, msgin);
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
59 clientcommand(source, strcat("say ", msgin));
66 namestr = playername(source.netname, source.team, (autocvar_g_chat_teamcolors));
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);
74 string colorprefix = (strdecolorize(namestr) == namestr) ? "^3" : "^7";
76 string msgstr = "", cmsgstr = "";
77 string privatemsgprefix = string_null;
78 int privatemsgprefixlen = 0;
81 bool found_me = false;
82 if(strstrofs(msgin, "/me", 0) >= 0)
85 string newnamestr = ((teamsay) ? strcat(colorstr, "(", colorprefix, namestr, colorstr, ")", "^7") : strcat(colorprefix, namestr, "^7"));
86 FOREACH_WORD(msgin, true,
88 if(strdecolorize(it) == "/me")
91 newmsgin = cons(newmsgin, newnamestr);
94 newmsgin = cons(newmsgin, it);
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");
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);
117 msgstr = strcat("\{1}\{13}", colorstr, "(Team) ", namestr, colorstr, ": ^5", msgin);
118 cmsgstr = strcat(colorstr, "(", colorprefix, namestr, colorstr, ")\n^5", msgin);
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);
131 msgstr = strcat(msgstr, (namestr != "") ? strcat(colorprefix, namestr, "^7: ") : "^7");
132 msgstr = strcat(msgstr, msgin);
136 msgstr = strcat(strreplace("\n", " ", msgstr), "\n"); // newlines only are good for centerprint
139 string fullmsgstr = msgstr;
140 string fullcmsgstr = cmsgstr;
144 var .float flood_field = floodcontrol_chat;
145 if(floodcontrol && source)
147 float flood_spl, flood_burst, flood_lmax;
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;
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;
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;
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!
172 // do flood control for the default line size
175 getWrappedLine_remaining = msgstr;
178 while(getWrappedLine_remaining && (!flood_lmax || lines <= flood_lmax))
180 msgstr = strcat(msgstr, " ", getWrappedLineLen(82.4289758859709, strlennocol)); // perl averagewidth.pl < gfx/vera-sans.width
183 msgstr = substring(msgstr, 1, strlen(msgstr) - 1);
185 if(getWrappedLine_remaining != "")
187 msgstr = strcat(msgstr, "\n");
191 if (time >= source.(flood_field))
193 source.(flood_field) = max(time - flood_burst * flood_spl, source.(flood_field)) + lines * flood_spl;
203 if (time >= source.(flood_field))
204 source.(flood_field) = max(time - flood_burst * flood_spl, source.(flood_field)) + flood_spl;
209 if (timeout_status == TIMEOUT_ACTIVE) // when game is paused, no flood protection
210 source.(flood_field) = flood = 0;
213 string sourcemsgstr, sourcecmsgstr;
214 if(flood == 2) // cannot happen for empty msgstr
216 if(autocvar_g_chat_flood_notify_flooder)
218 sourcemsgstr = strcat(msgstr, "\n^3FLOOD CONTROL: ^7message too long, trimmed\n");
223 sourcemsgstr = fullmsgstr;
224 sourcecmsgstr = fullcmsgstr;
230 sourcemsgstr = msgstr;
231 sourcecmsgstr = cmsgstr;
234 if (!privatesay && source && !(IS_PLAYER(source) || source.caplayer) && !game_stopped
235 && (teamsay || CHAT_NOSPECTATORS()))
237 teamsay = -1; // spectators
241 LOG_INFO("NOTE: ", playername(source.netname, source.team, IS_PLAYER(source)), "^7 is flooding.");
243 // build sourcemsgstr by cutting off a prefix and replacing it by the other one
245 sourcemsgstr = strcat(privatemsgprefix, substring(sourcemsgstr, privatemsgprefixlen, -1));
248 if(source && CS(source).muted)
250 // always fake the message
255 if (autocvar_g_chat_flood_notify_flooder)
257 sprint(source, strcat("^3FLOOD CONTROL: ^7wait ^1", ftos(source.(flood_field) - time), "^3 seconds\n"));
268 if (privatesay && source && !(IS_PLAYER(source) || source.caplayer) && !game_stopped
269 && (IS_PLAYER(privatesay) || privatesay.caplayer) && CHAT_NOSPECTATORS())
271 ret = -1; // just hide the message completely
274 MUTATOR_CALLHOOK(ChatMessage, source, ret);
275 ret = M_ARGV(1, int);
277 string event_log_msg = "";
279 if(sourcemsgstr != "" && ret != 0)
281 if(ret < 0) // faked message, because the player is muted
283 sprint(source, sourcemsgstr);
284 if(sourcecmsgstr != "" && !privatesay)
285 centerprint(source, sourcecmsgstr);
287 else if(privatesay) // private message, between 2 people only
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))
293 sprint(privatesay, msgstr);
295 centerprint(privatesay, cmsgstr);
298 else if ( teamsay && CS(source).active_minigame )
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), {
305 event_log_msg = sprintf(":chat_minigame:%d:%s:%s", source.playerid, CS(source).active_minigame.netname, msgin);
308 else if(teamsay > 0) // team message, only sent to team mates
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), {
317 centerprint(it, cmsgstr);
319 event_log_msg = sprintf(":chat_team:%d:%d:%s", source.playerid, source.team, strreplace("\n", " ", msgin));
321 else if(teamsay < 0) // spectator message, only sent to spectators
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), {
329 if(!play_chatsound(source, msgin))
330 event_log_msg = sprintf(":chat_spec:%d:%s", source.playerid, strreplace("\n", " ", msgin));
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));
339 FOREACH_CLIENT(IS_REAL_CLIENT(it) && it != source && !MUTATOR_CALLHOOK(ChatMessageTo, it, source), {
343 if(!play_chatsound(source, msgin))
344 event_log_msg = sprintf(":chat:%d:%s", source.playerid, strreplace("\n", " ", msgin));
348 if (autocvar_sv_eventlog && (event_log_msg != "")) {
349 GameLogEcho(event_log_msg);
355 bool play_chatsound(entity source, string msgin)
357 if(autocvar_sv_chat_sounds && CS(source).cvar_cl_chat_sounds) {
358 var .float flood_sound = floodcontrol_chatsound;
360 if (source.(flood_sound) < time - autocvar_sv_chat_sounds_flood) {
363 rawmsg = strreplace("\n", " ", msgin);
365 FOREACH_WORD(autocvar_sv_chat_sounds_list, it == rawmsg, { found = true; });
367 FOREACH_CLIENT(IS_REAL_CLIENT(it) && CS(it).cvar_cl_chat_sounds, {
369 WriteHeader(MSG_ONE, TE_CSQC_CHATSOUND);
370 WriteString(MSG_ONE, rawmsg);
372 source.(flood_sound) = time;
381 entity findnearest(vector point, bool checkitems, vector axismod)
386 IL_EACH(((checkitems) ? g_items : g_locations), ((checkitems) ? (it.target == "###item###") : (it.classname == "target_location")),
388 if ((it.items == IT_KEY1 || it.items == IT_KEY2) && it.target == "###item###")
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);
397 for (l = 0; l < num_nearest; ++l)
399 if (len < nearest_length[l])
403 // now i tells us where to insert at
404 // INSERTION SORT! YOU'VE SEEN IT! RUN!
405 if (l < NUM_NEAREST_ENTITIES)
407 for (int j = NUM_NEAREST_ENTITIES - 1; j >= l; --j)
409 nearest_length[j + 1] = nearest_length[j];
410 nearest_entity[j + 1] = nearest_entity[j];
412 nearest_length[l] = len;
413 nearest_entity[l] = it;
414 if (num_nearest < NUM_NEAREST_ENTITIES)
415 num_nearest = num_nearest + 1;
419 // now use the first one from our list that we can see
420 for (int j = 0; j < num_nearest; ++j)
422 traceline(point, nearest_entity[j].origin, true, NULL);
423 if (trace_fraction == 1)
426 LOG_TRACEF("Nearest point (%s) is not visible, using a visible one.", nearest_entity[0].netname);
427 return nearest_entity[j];
431 if (num_nearest == 0)
434 LOG_TRACE("Not seeing any location point, using nearest as fallback.");
436 dprint("Candidates were: ");
437 for(j = 0; j < num_nearest; ++j)
441 dprint(nearest_entity[j].netname);
446 return nearest_entity[0];
449 string NearestLocation(vector p)
451 string ret = "somewhere";
452 entity loc = findnearest(p, false, '1 1 1');
457 loc = findnearest(p, true, '1 1 4');
464 string PlayerHealth(entity this)
466 float myhealth = floor(GetResource(this, RES_HEALTH));
469 else if(myhealth == -2342 || (myhealth == 2342 && mapvote_initialized))
471 else if(myhealth <= 0 || IS_DEAD(this))
473 return ftos(myhealth);
476 string WeaponNameFromWeaponentity(entity this, .entity weaponentity)
478 entity wepent = this.(weaponentity);
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;
488 string formatmessage(entity this, string msg)
492 vector cursor = '0 0 0';
493 entity cursor_ent = NULL;
500 MUTATOR_CALLHOOK(PreFormatMessage, this, msg);
501 msg = M_ARGV(1, string);
505 break; // too many replacements
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!
524 WarpZone_crosshair_trace_plusvisibletriggers(this);
525 cursor = trace_endpos;
526 cursor_ent = trace_ent;
530 replacement = substring(msg, p, 2);
531 escape = substring(msg, p + 1, 1);
533 .entity weaponentity = weaponentities[0]; // TODO: unhardcode
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;
554 MUTATOR_CALLHOOK(FormatMessage, this, escape, replacement, msg);
555 replacement = M_ARGV(2, string);
560 msg = strcat(substring(msg, 0, p), replacement, substring(msg, p+2, strlen(msg) - (p+2)));
561 p = p + strlen(replacement);
567 void PrintToChat(entity client, string text)
569 text = strcat("\{1}^7", text, "\n");
570 sprint(client, text);
574 void DebugPrintToChat(entity client, string text)
576 if (autocvar_developer > 0)
578 PrintToChat(client, text);
583 void PrintToChatAll(string text)
585 text = strcat("\{1}^7", text, "\n");
590 void DebugPrintToChatAll(string text)
592 if (autocvar_developer > 0)
594 PrintToChatAll(text);
599 void PrintToChatTeam(int team_num, string text)
601 text = strcat("\{1}^7", text, "\n");
602 FOREACH_CLIENT(IS_REAL_CLIENT(it),
604 if (it.team == team_num)
612 void DebugPrintToChatTeam(int team_num, string text)
614 if (autocvar_developer > 0)
616 PrintToChatTeam(team_num, text);