--- /dev/null
- msgstr = strcat("\{1}\{13}* ", colorprefix, namestr, "^3 tells you: ^7");
+ #include "chat.qh"
+
+ #include <common/gamemodes/_mod.qh>
+ #include <common/mapobjects/target/location.qh>
+ #include <common/mapobjects/triggers.qh>
+ #include <common/teams.qh>
+ #include <common/util.qh>
+ #include <common/weapons/weapon.qh>
+ #include <common/wepent.qh>
+ #include <server/command/common.qh>
+ #include <server/gamelog.qh>
+ #include <server/main.qh>
+ #include <server/mapvoting.qh>
+ #include <server/mutators/_mod.qh>
+ #include <server/weapons/tracing.qh>
+ #include <server/world.qh>
+
+ /**
+ * message "": do not say, just test flood control
+ * return value:
+ * 1 = accept
+ * 0 = reject
+ * -1 = fake accept
+ */
+ int Say(entity source, int teamsay, entity privatesay, string msgin, bool floodcontrol)
+ {
+ if (!teamsay && !privatesay && substring(msgin, 0, 1) == " ")
+ msgin = substring(msgin, 1, -1); // work around DP say bug (say_team does not have this!)
+
+ if (source)
+ msgin = formatmessage(source, msgin);
+
+ string colorstr;
+ if (!(IS_PLAYER(source) || INGAME(source)))
+ colorstr = "^0"; // black for spectators
+ else if(teamplay)
+ colorstr = Team_ColorCode(source.team);
+ else
+ {
+ colorstr = "";
+ teamsay = false;
+ }
+
+ if (!source) {
+ colorstr = "";
+ teamsay = false;
+ }
+
+ if(msgin != "")
+ msgin = trigger_magicear_processmessage_forallears(source, teamsay, privatesay, msgin);
+
+ /*
+ * using bprint solves this... me stupid
+ // how can we prevent the message from appearing in a listen server?
+ // for now, just give "say" back and only handle say_team
+ if(!teamsay)
+ {
+ clientcommand(source, strcat("say ", msgin));
+ return;
+ }
+ */
+
+ string namestr = "";
+ if (source)
+ namestr = playername(source.netname, source.team, (autocvar_g_chat_teamcolors && IS_PLAYER(source)));
+
+ string colorprefix = (strdecolorize(namestr) == namestr) ? "^3" : "^7";
+
+ string msgstr = "", cmsgstr = "";
+ string privatemsgprefix = string_null;
+ int privatemsgprefixlen = 0;
+ if (msgin != "")
+ {
+ bool found_me = false;
+ if(strstrofs(msgin, "/me", 0) >= 0)
+ {
+ string newmsgin = "";
+ string newnamestr = ((teamsay) ? strcat(colorstr, "(", colorprefix, namestr, colorstr, ")", "^7") : strcat(colorprefix, namestr, "^7"));
+ FOREACH_WORD(msgin, true,
+ {
+ if(strdecolorize(it) == "/me")
+ {
+ found_me = true;
+ newmsgin = cons(newmsgin, newnamestr);
+ }
+ else
+ newmsgin = cons(newmsgin, it);
+ });
+ msgin = newmsgin;
+ }
+
+ if(privatesay)
+ {
- privatemsgprefix = strcat("\{1}\{13}* ^3You tell ", playername(privatesay.netname, privatesay.team, (autocvar_g_chat_teamcolors && IS_PLAYER(privatesay))), ": ^7");
++ msgstr = strcat("\{1}\r* ", colorprefix, namestr, "^3 tells you: ^7");
+ privatemsgprefixlen = strlen(msgstr);
+ msgstr = strcat(msgstr, msgin);
+ cmsgstr = strcat(colorstr, colorprefix, namestr, "^3 tells you:\n^7", msgin);
- msgstr = strcat("\{1}\{13}^4* ", "^7", msgin);
++ privatemsgprefix = strcat("\{1}\r* ^3You tell ", playername(privatesay.netname, privatesay.team, (autocvar_g_chat_teamcolors && IS_PLAYER(privatesay))), ": ^7");
+ }
+ else if(teamsay)
+ {
+ if(found_me)
+ {
+ //msgin = strreplace("/me", "", msgin);
+ //msgin = substring(msgin, 3, strlen(msgin));
+ //msgin = strreplace("/me", strcat(colorstr, "(", colorprefix, namestr, colorstr, ")^7"), msgin);
- msgstr = strcat("\{1}\{13}", colorstr, "(", colorprefix, namestr, colorstr, ") ^7", msgin);
++ msgstr = strcat("\{1}\r^4* ", "^7", msgin);
+ }
+ else
++ msgstr = strcat("\{1}\r", colorstr, "(", colorprefix, namestr, colorstr, ") ^7", msgin);
+ cmsgstr = strcat(colorstr, "(", colorprefix, namestr, colorstr, ")\n^7", msgin);
+ }
+ else
+ {
+ if(found_me)
+ {
+ //msgin = strreplace("/me", "", msgin);
+ //msgin = substring(msgin, 3, strlen(msgin));
+ //msgin = strreplace("/me", strcat(colorprefix, namestr), msgin);
+ msgstr = strcat("\{1}^4* ^7", msgin);
+ }
+ else {
+ msgstr = "\{1}";
+ msgstr = strcat(msgstr, (namestr != "") ? strcat(colorprefix, namestr, "^7: ") : "^7");
+ msgstr = strcat(msgstr, msgin);
+ }
+ cmsgstr = "";
+ }
+ msgstr = strcat(strreplace("\n", " ", msgstr), "\n"); // newlines only are good for centerprint
+ }
+
+ string fullmsgstr = msgstr;
+ string fullcmsgstr = cmsgstr;
+
+ // FLOOD CONTROL
+ int flood = 0;
+ var .float flood_field = floodcontrol_chat;
+ if(floodcontrol && source)
+ {
+ float flood_spl, flood_burst, flood_lmax;
+ if(privatesay)
+ {
+ flood_spl = autocvar_g_chat_flood_spl_tell;
+ flood_burst = autocvar_g_chat_flood_burst_tell;
+ flood_lmax = autocvar_g_chat_flood_lmax_tell;
+ flood_field = floodcontrol_chattell;
+ }
+ else if(teamsay)
+ {
+ flood_spl = autocvar_g_chat_flood_spl_team;
+ flood_burst = autocvar_g_chat_flood_burst_team;
+ flood_lmax = autocvar_g_chat_flood_lmax_team;
+ flood_field = floodcontrol_chatteam;
+ }
+ else
+ {
+ flood_spl = autocvar_g_chat_flood_spl;
+ flood_burst = autocvar_g_chat_flood_burst;
+ flood_lmax = autocvar_g_chat_flood_lmax;
+ flood_field = floodcontrol_chat;
+ }
+ flood_burst = max(0, flood_burst - 1);
+ // to match explanation in default.cfg, a value of 3 must allow three-line bursts and not four!
+
+ // do flood control for the default line size
+ if(msgstr != "")
+ {
+ getWrappedLine_remaining = msgstr;
+ msgstr = "";
+ int lines = 0;
+ while(getWrappedLine_remaining && (!flood_lmax || lines <= flood_lmax))
+ {
+ msgstr = strcat(msgstr, " ", getWrappedLineLen(82.4289758859709, strlennocol)); // perl averagewidth.pl < gfx/vera-sans.width
+ ++lines;
+ }
+ msgstr = substring(msgstr, 1, strlen(msgstr) - 1);
+
+ if(getWrappedLine_remaining != "")
+ {
+ msgstr = strcat(msgstr, "\n");
+ flood = 2;
+ }
+
+ if (time >= source.(flood_field))
+ {
+ source.(flood_field) = max(time - flood_burst * flood_spl, source.(flood_field)) + lines * flood_spl;
+ }
+ else
+ {
+ flood = 1;
+ msgstr = fullmsgstr;
+ }
+ }
+ else
+ {
+ if (time >= source.(flood_field))
+ source.(flood_field) = max(time - flood_burst * flood_spl, source.(flood_field)) + flood_spl;
+ else
+ flood = 1;
+ }
+
+ if (timeout_status == TIMEOUT_ACTIVE) // when game is paused, no flood protection
+ source.(flood_field) = flood = 0;
+ }
+
+ string sourcemsgstr, sourcecmsgstr;
+ if(flood == 2) // cannot happen for empty msgstr
+ {
+ if(autocvar_g_chat_flood_notify_flooder)
+ {
+ sourcemsgstr = strcat(msgstr, "\n^3FLOOD CONTROL: ^7message too long, trimmed\n");
+ sourcecmsgstr = "";
+ }
+ else
+ {
+ sourcemsgstr = fullmsgstr;
+ sourcecmsgstr = fullcmsgstr;
+ }
+ cmsgstr = "";
+ }
+ else
+ {
+ sourcemsgstr = msgstr;
+ sourcecmsgstr = cmsgstr;
+ }
+
+ if (!privatesay && source && !(IS_PLAYER(source) || INGAME(source)) && !game_stopped
+ && (teamsay || CHAT_NOSPECTATORS()))
+ {
+ teamsay = -1; // spectators
+ }
+
+ if(flood)
+ LOG_INFO("NOTE: ", playername(source.netname, source.team, IS_PLAYER(source)), "^7 is flooding.");
+
+ // build sourcemsgstr by cutting off a prefix and replacing it by the other one
+ if(privatesay)
+ sourcemsgstr = strcat(privatemsgprefix, substring(sourcemsgstr, privatemsgprefixlen, -1));
+
+ int ret;
+ if(source && CS(source).muted)
+ {
+ // always fake the message
+ ret = -1;
+ }
+ else if(flood == 1)
+ {
+ if (autocvar_g_chat_flood_notify_flooder)
+ {
+ sprint(source, strcat("^3FLOOD CONTROL: ^7wait ^1", ftos(source.(flood_field) - time), "^3 seconds\n"));
+ ret = 0;
+ }
+ else
+ ret = -1;
+ }
+ else
+ {
+ ret = 1;
+ }
+
+ if (privatesay && source && !(IS_PLAYER(source) || INGAME(source)) && !game_stopped
+ && (IS_PLAYER(privatesay) || INGAME(privatesay)) && CHAT_NOSPECTATORS())
+ {
+ ret = -1; // just hide the message completely
+ }
+
+ MUTATOR_CALLHOOK(ChatMessage, source, ret);
+ ret = M_ARGV(1, int);
+
+ string event_log_msg = "";
+
+ if(sourcemsgstr != "" && ret != 0)
+ {
+ if(ret < 0) // faked message, because the player is muted
+ {
+ sprint(source, sourcemsgstr);
+ if(sourcecmsgstr != "" && !privatesay)
+ centerprint(source, sourcecmsgstr);
+ }
+ else if(privatesay) // private message, between 2 people only
+ {
+ sprint(source, sourcemsgstr);
+ if (!autocvar_g_chat_tellprivacy) { dedicated_print(msgstr); } // send to server console too if "tellprivacy" is disabled
+ if(!MUTATOR_CALLHOOK(ChatMessageTo, privatesay, source))
+ {
+ sprint(privatesay, msgstr);
+ if(cmsgstr != "")
+ centerprint(privatesay, cmsgstr);
+ }
+ }
+ else if ( teamsay && CS(source).active_minigame )
+ {
+ sprint(source, sourcemsgstr);
+ dedicated_print(msgstr); // send to server console too
+ FOREACH_CLIENT(IS_REAL_CLIENT(it) && it != source && CS(it).active_minigame == CS(source).active_minigame && !MUTATOR_CALLHOOK(ChatMessageTo, it, source), {
+ sprint(it, msgstr);
+ });
+ event_log_msg = sprintf(":chat_minigame:%d:%s:%s", source.playerid, CS(source).active_minigame.netname, msgin);
+
+ }
+ else if(teamsay > 0) // team message, only sent to team mates
+ {
+ sprint(source, sourcemsgstr);
+ dedicated_print(msgstr); // send to server console too
+ if(sourcecmsgstr != "")
+ centerprint(source, sourcecmsgstr);
+ FOREACH_CLIENT((IS_PLAYER(it) || INGAME(it)) && IS_REAL_CLIENT(it) && it != source && it.team == source.team && !MUTATOR_CALLHOOK(ChatMessageTo, it, source), {
+ sprint(it, msgstr);
+ if(cmsgstr != "")
+ centerprint(it, cmsgstr);
+ });
+ event_log_msg = sprintf(":chat_team:%d:%d:%s", source.playerid, source.team, strreplace("\n", " ", msgin));
+ }
+ else if(teamsay < 0) // spectator message, only sent to spectators
+ {
+ sprint(source, sourcemsgstr);
+ dedicated_print(msgstr); // send to server console too
+ FOREACH_CLIENT(!(IS_PLAYER(it) || INGAME(it)) && IS_REAL_CLIENT(it) && it != source && !MUTATOR_CALLHOOK(ChatMessageTo, it, source), {
+ sprint(it, msgstr);
+ });
+ event_log_msg = sprintf(":chat_spec:%d:%s", source.playerid, strreplace("\n", " ", msgin));
+ }
+ else
+ {
+ if (source) {
+ sprint(source, sourcemsgstr);
+ dedicated_print(msgstr); // send to server console too
+ MX_Say(strcat(playername(source.netname, source.team, IS_PLAYER(source)), "^7: ", msgin));
+ }
+ FOREACH_CLIENT(IS_REAL_CLIENT(it) && it != source && !MUTATOR_CALLHOOK(ChatMessageTo, it, source), {
+ sprint(it, msgstr);
+ });
+ event_log_msg = sprintf(":chat:%d:%s", source.playerid, strreplace("\n", " ", msgin));
+ }
+ }
+
+ if (autocvar_sv_eventlog && (event_log_msg != "")) {
+ GameLogEcho(event_log_msg);
+ }
+
+ return ret;
+ }
+
+ entity findnearest(vector point, bool checkitems, vector axismod)
+ {
+ vector dist;
+ int num_nearest = 0;
+
+ IL_EACH(((checkitems) ? g_items : g_locations), ((checkitems) ? (it.target == "###item###") : (it.classname == "target_location")),
+ {
+ if ((it.items == IT_KEY1 || it.items == IT_KEY2) && it.target == "###item###")
+ dist = it.oldorigin;
+ else
+ dist = it.origin;
+ dist = dist - point;
+ dist = dist.x * axismod.x * '1 0 0' + dist.y * axismod.y * '0 1 0' + dist.z * axismod.z * '0 0 1';
+ float len = vlen2(dist);
+
+ int l;
+ for (l = 0; l < num_nearest; ++l)
+ {
+ if (len < nearest_length[l])
+ break;
+ }
+
+ // now i tells us where to insert at
+ // INSERTION SORT! YOU'VE SEEN IT! RUN!
+ if (l < NUM_NEAREST_ENTITIES)
+ {
+ for (int j = NUM_NEAREST_ENTITIES - 1; j >= l; --j)
+ {
+ nearest_length[j + 1] = nearest_length[j];
+ nearest_entity[j + 1] = nearest_entity[j];
+ }
+ nearest_length[l] = len;
+ nearest_entity[l] = it;
+ if (num_nearest < NUM_NEAREST_ENTITIES)
+ num_nearest = num_nearest + 1;
+ }
+ });
+
+ // now use the first one from our list that we can see
+ for (int j = 0; j < num_nearest; ++j)
+ {
+ traceline(point, nearest_entity[j].origin, true, NULL);
+ if (trace_fraction == 1)
+ {
+ if (j != 0)
+ LOG_TRACEF("Nearest point (%s) is not visible, using a visible one.", nearest_entity[0].netname);
+ return nearest_entity[j];
+ }
+ }
+
+ if (num_nearest == 0)
+ return NULL;
+
+ LOG_TRACE("Not seeing any location point, using nearest as fallback.");
+ /* DEBUGGING CODE:
+ dprint("Candidates were: ");
+ for(j = 0; j < num_nearest; ++j)
+ {
+ if(j != 0)
+ dprint(", ");
+ dprint(nearest_entity[j].netname);
+ }
+ dprint("\n");
+ */
+
+ return nearest_entity[0];
+ }
+
+ string NearestLocation(vector p)
+ {
+ string ret = "somewhere";
+ entity loc = findnearest(p, false, '1 1 1');
+ if (loc)
+ ret = loc.message;
+ else
+ {
+ loc = findnearest(p, true, '1 1 4');
+ if (loc)
+ ret = loc.netname;
+ }
+ return ret;
+ }
+
+ string PlayerHealth(entity this)
+ {
+ float myhealth = floor(GetResource(this, RES_HEALTH));
+ if(myhealth == -666)
+ return "spectating";
+ else if(myhealth == -2342 || (myhealth == 2342 && mapvote_initialized))
+ return "observing";
+ else if(myhealth <= 0 || IS_DEAD(this))
+ return "dead";
+ return ftos(myhealth);
+ }
+
+ string WeaponNameFromWeaponentity(entity this, .entity weaponentity)
+ {
+ entity wepent = this.(weaponentity);
+ if(!wepent)
+ return "none";
+ else if(wepent.m_weapon != WEP_Null)
+ return wepent.m_weapon.m_name;
+ else if(wepent.m_switchweapon != WEP_Null)
+ return wepent.m_switchweapon.m_name;
+ return "none"; //REGISTRY_GET(Weapons, wepent.cnt).m_name;
+ }
+
+ string formatmessage(entity this, string msg)
+ {
+ float p, p1, p2;
+ float n;
+ vector cursor = '0 0 0';
+ entity cursor_ent = NULL;
+ string escape;
+ string replacement;
+ p = 0;
+ n = 7;
+ bool traced = false;
+
+ MUTATOR_CALLHOOK(PreFormatMessage, this, msg);
+ msg = M_ARGV(1, string);
+
+ while (1) {
+ if (n < 1)
+ break; // too many replacements
+
+ n = n - 1;
+ p1 = strstrofs(msg, "%", p); // NOTE: this destroys msg as it's a tempstring!
+ p2 = strstrofs(msg, "\\", p); // NOTE: this destroys msg as it's a tempstring!
+
+ if (p1 < 0)
+ p1 = p2;
+
+ if (p2 < 0)
+ p2 = p1;
+
+ p = min(p1, p2);
+
+ if (p < 0)
+ break;
+
+ if(!traced)
+ {
+ WarpZone_crosshair_trace_plusvisibletriggers(this);
+ cursor = trace_endpos;
+ cursor_ent = trace_ent;
+ traced = true;
+ }
+
+ replacement = substring(msg, p, 2);
+ escape = substring(msg, p + 1, 1);
+
+ .entity weaponentity = weaponentities[0]; // TODO: unhardcode
+
+ switch(escape)
+ {
+ case "%": replacement = "%"; break;
+ case "\\":replacement = "\\"; break;
+ case "n": replacement = "\n"; break;
+ case "a": replacement = ftos(floor(GetResource(this, RES_ARMOR))); break;
+ case "h": replacement = PlayerHealth(this); break;
+ case "l": replacement = NearestLocation(this.origin); break;
+ case "y": replacement = NearestLocation(cursor); break;
+ case "d": replacement = NearestLocation(this.death_origin); break;
+ case "w": replacement = WeaponNameFromWeaponentity(this, weaponentity); break;
+ case "W": replacement = GetAmmoName(this.(weaponentity).m_weapon.ammo_type); break;
+ case "x": replacement = ((cursor_ent.netname == "" || !cursor_ent) ? "nothing" : cursor_ent.netname); break;
+ case "s": replacement = ftos(vlen(this.velocity - this.velocity_z * '0 0 1')); break;
+ case "S": replacement = ftos(vlen(this.velocity)); break;
+ case "t": replacement = seconds_tostring(ceil(max(0, autocvar_timelimit * 60 + game_starttime - time))); break;
+ case "T": replacement = seconds_tostring(floor(time - game_starttime)); break;
+ default:
+ {
+ MUTATOR_CALLHOOK(FormatMessage, this, escape, replacement, msg);
+ replacement = M_ARGV(2, string);
+ break;
+ }
+ }
+
+ msg = strcat(substring(msg, 0, p), replacement, substring(msg, p+2, strlen(msg) - (p+2)));
+ p = p + strlen(replacement);
+ }
+ return msg;
+ }
+
+ ERASEABLE
+ void PrintToChat(entity client, string text)
+ {
+ text = strcat("\{1}^7", text, "\n");
+ sprint(client, text);
+ }
+
+ ERASEABLE
+ void DebugPrintToChat(entity client, string text)
+ {
+ if (autocvar_developer > 0)
+ {
+ PrintToChat(client, text);
+ }
+ }
+
+ ERASEABLE
+ void PrintToChatAll(string text)
+ {
+ text = strcat("\{1}^7", text, "\n");
+ bprint(text);
+ }
+
+ ERASEABLE
+ void DebugPrintToChatAll(string text)
+ {
+ if (autocvar_developer > 0)
+ {
+ PrintToChatAll(text);
+ }
+ }
+
+ ERASEABLE
+ void PrintToChatTeam(int team_num, string text)
+ {
+ text = strcat("\{1}^7", text, "\n");
+ FOREACH_CLIENT(IS_REAL_CLIENT(it),
+ {
+ if (it.team == team_num)
+ {
+ sprint(it, text);
+ }
+ });
+ }
+
+ ERASEABLE
+ void DebugPrintToChatTeam(int team_num, string text)
+ {
+ if (autocvar_developer > 0)
+ {
+ PrintToChatTeam(team_num, text);
+ }
+ }