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>
19 * message "": do not say, just test flood control
25 int Say(entity source, int teamsay, entity privatesay, string msgin, bool floodcontrol)
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!)
31 msgin = formatmessage(source, msgin);
34 if (!(IS_PLAYER(source) || INGAME(source)))
35 colorstr = "^0"; // black for spectators
37 colorstr = Team_ColorCode(source.team);
50 msgin = trigger_magicear_processmessage_forallears(source, teamsay, privatesay, msgin);
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
58 clientcommand(source, strcat("say ", msgin));
65 namestr = playername(source.netname, source.team, (autocvar_g_chat_teamcolors && IS_PLAYER(source)));
67 string colorprefix = (strdecolorize(namestr) == namestr) ? "^3" : "^7";
69 string msgstr = "", cmsgstr = "";
70 string privatemsgprefix = string_null;
71 int privatemsgprefixlen = 0;
74 bool found_me = false;
75 if(strstrofs(msgin, "/me", 0) >= 0)
78 string newnamestr = ((teamsay) ? strcat(colorstr, "(", colorprefix, namestr, colorstr, ")", "^7") : strcat(colorprefix, namestr, "^7"));
79 FOREACH_WORD(msgin, true,
81 if(strdecolorize(it) == "/me")
84 newmsgin = cons(newmsgin, newnamestr);
87 newmsgin = cons(newmsgin, it);
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");
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);
110 msgstr = strcat("\{1}\{13}", colorstr, "(", colorprefix, namestr, colorstr, ") ^7", msgin);
111 cmsgstr = strcat(colorstr, "(", colorprefix, namestr, colorstr, ")\n^7", msgin);
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);
124 msgstr = strcat(msgstr, (namestr != "") ? strcat(colorprefix, namestr, "^7: ") : "^7");
125 msgstr = strcat(msgstr, msgin);
129 msgstr = strcat(strreplace("\n", " ", msgstr), "\n"); // newlines only are good for centerprint
132 string fullmsgstr = msgstr;
133 string fullcmsgstr = cmsgstr;
137 var .float flood_field = floodcontrol_chat;
138 if(floodcontrol && source)
140 float flood_spl, flood_burst, flood_lmax;
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;
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;
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;
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!
165 // do flood control for the default line size
168 getWrappedLine_remaining = msgstr;
171 while(getWrappedLine_remaining && (!flood_lmax || lines <= flood_lmax))
173 msgstr = strcat(msgstr, " ", getWrappedLineLen(82.4289758859709, strlennocol)); // perl averagewidth.pl < gfx/vera-sans.width
176 msgstr = substring(msgstr, 1, strlen(msgstr) - 1);
178 if(getWrappedLine_remaining != "")
180 msgstr = strcat(msgstr, "\n");
184 if (time >= source.(flood_field))
186 source.(flood_field) = max(time - flood_burst * flood_spl, source.(flood_field)) + lines * flood_spl;
196 if (time >= source.(flood_field))
197 source.(flood_field) = max(time - flood_burst * flood_spl, source.(flood_field)) + flood_spl;
202 if (timeout_status == TIMEOUT_ACTIVE) // when game is paused, no flood protection
203 source.(flood_field) = flood = 0;
206 string sourcemsgstr, sourcecmsgstr;
207 if(flood == 2) // cannot happen for empty msgstr
209 if(autocvar_g_chat_flood_notify_flooder)
211 sourcemsgstr = strcat(msgstr, "\n^3FLOOD CONTROL: ^7message too long, trimmed\n");
216 sourcemsgstr = fullmsgstr;
217 sourcecmsgstr = fullcmsgstr;
223 sourcemsgstr = msgstr;
224 sourcecmsgstr = cmsgstr;
227 if (!privatesay && source && !(IS_PLAYER(source) || INGAME(source)) && !game_stopped
228 && (teamsay || CHAT_NOSPECTATORS()))
230 teamsay = -1; // spectators
234 LOG_INFO("NOTE: ", playername(source.netname, source.team, IS_PLAYER(source)), "^7 is flooding.");
236 // build sourcemsgstr by cutting off a prefix and replacing it by the other one
238 sourcemsgstr = strcat(privatemsgprefix, substring(sourcemsgstr, privatemsgprefixlen, -1));
241 if(source && CS(source).muted)
243 // always fake the message
248 if (autocvar_g_chat_flood_notify_flooder)
250 sprint(source, strcat("^3FLOOD CONTROL: ^7wait ^1", ftos(source.(flood_field) - time), "^3 seconds\n"));
261 if (privatesay && source && !(IS_PLAYER(source) || INGAME(source)) && !game_stopped
262 && (IS_PLAYER(privatesay) || INGAME(privatesay)) && CHAT_NOSPECTATORS())
264 ret = -1; // just hide the message completely
267 MUTATOR_CALLHOOK(ChatMessage, source, ret);
268 ret = M_ARGV(1, int);
270 string event_log_msg = "";
272 if(sourcemsgstr != "" && ret != 0)
274 if(ret < 0) // faked message, because the player is muted
276 sprint(source, sourcemsgstr);
277 if(sourcecmsgstr != "" && !privatesay)
278 centerprint(source, sourcecmsgstr);
280 else if(privatesay) // private message, between 2 people only
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))
286 sprint(privatesay, msgstr);
288 centerprint(privatesay, cmsgstr);
291 else if ( teamsay && CS(source).active_minigame )
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), {
298 event_log_msg = sprintf(":chat_minigame:%d:%s:%s", source.playerid, CS(source).active_minigame.netname, msgin);
301 else if(teamsay > 0) // team message, only sent to team mates
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) || INGAME(it)) && IS_REAL_CLIENT(it) && it != source && it.team == source.team && !MUTATOR_CALLHOOK(ChatMessageTo, it, source), {
310 centerprint(it, cmsgstr);
312 event_log_msg = sprintf(":chat_team:%d:%d:%s", source.playerid, source.team, strreplace("\n", " ", msgin));
314 else if(teamsay < 0) // spectator message, only sent to spectators
316 sprint(source, sourcemsgstr);
317 dedicated_print(msgstr); // send to server console too
318 FOREACH_CLIENT(!(IS_PLAYER(it) || INGAME(it)) && IS_REAL_CLIENT(it) && it != source && !MUTATOR_CALLHOOK(ChatMessageTo, it, source), {
321 event_log_msg = sprintf(":chat_spec:%d:%s", source.playerid, strreplace("\n", " ", msgin));
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));
330 FOREACH_CLIENT(IS_REAL_CLIENT(it) && it != source && !MUTATOR_CALLHOOK(ChatMessageTo, it, source), {
333 event_log_msg = sprintf(":chat:%d:%s", source.playerid, strreplace("\n", " ", msgin));
337 if (autocvar_sv_eventlog && (event_log_msg != "")) {
338 GameLogEcho(event_log_msg);
344 entity findnearest(vector point, bool checkitems, vector axismod)
349 IL_EACH(((checkitems) ? g_items : g_locations), ((checkitems) ? (it.target == "###item###") : (it.classname == "target_location")),
351 if ((it.items == IT_KEY1 || it.items == IT_KEY2) && it.target == "###item###")
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);
360 for (l = 0; l < num_nearest; ++l)
362 if (len < nearest_length[l])
366 // now i tells us where to insert at
367 // INSERTION SORT! YOU'VE SEEN IT! RUN!
368 if (l < NUM_NEAREST_ENTITIES)
370 for (int j = NUM_NEAREST_ENTITIES - 1; j >= l; --j)
372 nearest_length[j + 1] = nearest_length[j];
373 nearest_entity[j + 1] = nearest_entity[j];
375 nearest_length[l] = len;
376 nearest_entity[l] = it;
377 if (num_nearest < NUM_NEAREST_ENTITIES)
378 num_nearest = num_nearest + 1;
382 // now use the first one from our list that we can see
383 for (int j = 0; j < num_nearest; ++j)
385 traceline(point, nearest_entity[j].origin, true, NULL);
386 if (trace_fraction == 1)
389 LOG_TRACEF("Nearest point (%s) is not visible, using a visible one.", nearest_entity[0].netname);
390 return nearest_entity[j];
394 if (num_nearest == 0)
397 LOG_TRACE("Not seeing any location point, using nearest as fallback.");
399 dprint("Candidates were: ");
400 for(j = 0; j < num_nearest; ++j)
404 dprint(nearest_entity[j].netname);
409 return nearest_entity[0];
412 string NearestLocation(vector p)
414 string ret = "somewhere";
415 entity loc = findnearest(p, false, '1 1 1');
420 loc = findnearest(p, true, '1 1 4');
427 string PlayerHealth(entity this)
429 float myhealth = floor(GetResource(this, RES_HEALTH));
432 else if(myhealth == -2342 || (myhealth == 2342 && mapvote_initialized))
434 else if(myhealth <= 0 || IS_DEAD(this))
436 return ftos(myhealth);
439 string WeaponNameFromWeaponentity(entity this, .entity weaponentity)
441 entity wepent = this.(weaponentity);
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;
451 string formatmessage(entity this, string msg)
455 vector cursor = '0 0 0';
456 entity cursor_ent = NULL;
463 MUTATOR_CALLHOOK(PreFormatMessage, this, msg);
464 msg = M_ARGV(1, string);
468 break; // too many replacements
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!
487 WarpZone_crosshair_trace_plusvisibletriggers(this);
488 cursor = trace_endpos;
489 cursor_ent = trace_ent;
493 replacement = substring(msg, p, 2);
494 escape = substring(msg, p + 1, 1);
496 .entity weaponentity = weaponentities[0]; // TODO: unhardcode
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;
517 MUTATOR_CALLHOOK(FormatMessage, this, escape, replacement, msg);
518 replacement = M_ARGV(2, string);
523 msg = strcat(substring(msg, 0, p), replacement, substring(msg, p+2, strlen(msg) - (p+2)));
524 p = p + strlen(replacement);
530 void PrintToChat(entity client, string text)
532 text = strcat("\{1}^7", text, "\n");
533 sprint(client, text);
537 void DebugPrintToChat(entity client, string text)
539 if (autocvar_developer > 0)
541 PrintToChat(client, text);
546 void PrintToChatAll(string text)
548 text = strcat("\{1}^7", text, "\n");
553 void DebugPrintToChatAll(string text)
555 if (autocvar_developer > 0)
557 PrintToChatAll(text);
562 void PrintToChatTeam(int team_num, string text)
564 text = strcat("\{1}^7", text, "\n");
565 FOREACH_CLIENT(IS_REAL_CLIENT(it),
567 if (it.team == team_num)
575 void DebugPrintToChatTeam(int team_num, string text)
577 if (autocvar_developer > 0)
579 PrintToChatTeam(team_num, text);