diff --git a/config/game.cfg b/config/game.cfg index 1d14323f..cf768850 100644 --- a/config/game.cfg +++ b/config/game.cfg @@ -303,12 +303,12 @@ copymapcfg = [ ] ] -chatsay = [ inputcommand "" [ say $commandbuf ] "[Chat]" s ] +chatsay = [ inputcommand "" [ say $commandbuf ] "[Chat]" n ] chatteam = [ if (|| $isspectator [= $getteam 0]) [ chatsay ] [ - inputcommand "" [sayteam $commandbuf] (+s "[" $getteamtextcode "Team Chat^f7]") s + inputcommand "" [sayteam $commandbuf] (+s "[" $getteamtextcode "Team Chat^f7]") n ] ] chatexec = [ inputcommand "" [ commandbuf ] "[^f4Console^f7]" s ] diff --git a/source/engine/console.cpp b/source/engine/console.cpp index db3c69d6..892913df 100644 --- a/source/engine/console.cpp +++ b/source/engine/console.cpp @@ -9,7 +9,7 @@ reversequeue conlines; int commandmillis = -1; string commandbuf; char *commandaction = NULL, *commandprompt = NULL; -enum { CF_COMPLETE = 1<<0, CF_EXECUTE = 1<<1 }; +enum { CF_COMPLETE = 1<<0, CF_EXECUTE = 1<<1, CF_NAMECOMPLETE = 1<<2 }; int commandflags = 0, commandpos = -1; VARFP(maxcon, 10, 200, MAXCONLINES, { while(conlines.length() > maxcon) delete[] conlines.pop().line; }); @@ -368,6 +368,7 @@ void inputcommand(char *init, char *action = NULL, char *prompt = NULL, char *fl case 'c': commandflags |= CF_COMPLETE; break; case 'x': commandflags |= CF_EXECUTE; break; case 's': commandflags |= CF_COMPLETE|CF_EXECUTE; break; + case 'n': commandflags |= CF_COMPLETE|CF_EXECUTE|CF_NAMECOMPLETE; break; } else if(init) commandflags |= CF_COMPLETE|CF_EXECUTE; if(!isfullconsoletoggled && fullconsolecommand) @@ -615,6 +616,26 @@ static char *skipwordrev(char *s, int n = -1) return e+1; } +/* don't try to complete names if: + - first character is a slash (already typing a command) or the null character (empty string) + - the cursor is at position 0 (there must be a '@' character before the cursor) + - there are spaces between the '@' symbol and the cursor (the '@mention' must be a single word) +*/ +static bool may_complete_names() +{ + if(commandbuf[0] == '/' || !commandbuf[0] || commandpos == 0) return false; + for(const char *p = &commandbuf[commandpos > 0 ? (commandpos-1) : (strlen(commandbuf)-1)]; p >= commandbuf; --p) + { + if(isspace(*p)) return false; + if(*p == '@') + { + if(p == commandbuf || isspace(p[-1])) return true; + continue; // the '@' symbol is not at the start of the buffer and does not follow a space: it could be part of the name, keep searching + } + } + return false; +} + bool consolekey(int code, bool isdown) { if(commandmillis < 0) return false; @@ -703,9 +724,13 @@ bool consolekey(int code, bool isdown) break; case SDLK_TAB: - if(commandflags&CF_COMPLETE) + if(commandflags&CF_NAMECOMPLETE && may_complete_names()) // disable name completion if typing a command { - complete(commandbuf, sizeof(commandbuf), commandflags&CF_EXECUTE ? "/" : NULL); + complete(commandbuf, sizeof(commandbuf), NULL, true); + } + else if(commandflags&CF_COMPLETE) + { + complete(commandbuf, sizeof(commandbuf), commandflags&CF_EXECUTE ? "/" : NULL, false); if(commandpos>=0 && commandpos>=(int)strlen(commandbuf)) commandpos = -1; } break; @@ -924,7 +949,7 @@ COMMANDN(complete, addfilecomplete, "sss"); COMMANDN(varcomplete, addvarcomplete, "sss"); COMMANDN(listcomplete, addlistcomplete, "ss"); -void complete(char *s, int maxlen, const char *cmdprefix) +void complete(char *s, int maxlen, const char *cmdprefix, bool names) { int cmdlen = 0; if(cmdprefix) @@ -935,34 +960,61 @@ void complete(char *s, int maxlen, const char *cmdprefix) if(!s[cmdlen]) return; if(!completesize) { completesize = (int)strlen(&s[cmdlen]); DELETEA(lastcomplete); } + const char *nextcomplete = NULL; + + if(names) + { + const char *last_at = NULL; + for(const char *p = s; *p; ++p) + { + if(*p == '@' && (p == s || isspace(p[-1]))) last_at = p; + } + if(last_at) // complete using player names + { + cmdprefix = s; + cmdlen = last_at - s + 1; + char *end = strchr(&s[cmdlen], ' '); + const char *found = game::completename(&s[last_at - s + 1], completesize-cmdlen-(end ? strlen(end) : 0), lastcomplete, nextcomplete); + if(found) + { + char *found_nospace = newstring(found); + for(char *p = found_nospace; *p; ++p) if(*p == ' ') *p = '_'; + nextcomplete = end ? tempformatstring("%s%s", found_nospace, end) : found_nospace; + delete[] found_nospace; + } + } + } + filesval *f = NULL; - if(completesize) + if(completesize && !names) { char *end = strchr(&s[cmdlen], ' '); if(end) f = completions.find(stringslice(&s[cmdlen], end), NULL); } - const char *nextcomplete = NULL; - if(f) // complete using filenames + if(!names) { - int commandsize = strchr(&s[cmdlen], ' ')+1-s; - f->update(); - loopv(f->files) + if(f) // complete using filenames { - if(strncmp(f->files[i], &s[commandsize], completesize+cmdlen-commandsize)==0 && - (!lastcomplete || strcmp(f->files[i], lastcomplete) > 0) && (!nextcomplete || strcmp(f->files[i], nextcomplete) < 0)) - nextcomplete = f->files[i]; + int commandsize = strchr(&s[cmdlen], ' ')+1-s; + f->update(); + loopv(f->files) + { + if(strncmp(f->files[i], &s[commandsize], completesize+cmdlen-commandsize)==0 && + (!lastcomplete || strcmp(f->files[i], lastcomplete) > 0) && (!nextcomplete || strcmp(f->files[i], nextcomplete) < 0)) + nextcomplete = f->files[i]; + } + cmdprefix = s; + cmdlen = commandsize; + } + else // complete using command names + { + enumerate(idents, ident, id, + if(strncmp(id.name, &s[cmdlen], completesize)==0 && + (!lastcomplete || strcmp(id.name, lastcomplete) > 0) && (!nextcomplete || strcmp(id.name, nextcomplete) < 0)) + nextcomplete = id.name; + ); } - cmdprefix = s; - cmdlen = commandsize; - } - else // complete using command names - { - enumerate(idents, ident, id, - if(strncmp(id.name, &s[cmdlen], completesize)==0 && - (!lastcomplete || strcmp(id.name, lastcomplete) > 0) && (!nextcomplete || strcmp(id.name, nextcomplete) < 0)) - nextcomplete = id.name; - ); } DELETEA(lastcomplete); if(nextcomplete) diff --git a/source/engine/engine.h b/source/engine/engine.h index 7e2ad31a..268ebfa1 100644 --- a/source/engine/engine.h +++ b/source/engine/engine.h @@ -591,7 +591,7 @@ extern float renderconsole(float w, float h, float abovehud); extern void conoutf(const char *s, ...) PRINTFARGS(1, 2); extern void conoutf(int type, const char *s, ...) PRINTFARGS(2, 3); extern void resetcomplete(); -extern void complete(char *s, int maxlen, const char *cmdprefix); +extern void complete(char *s, int maxlen, const char *cmdprefix, bool names = false); const char *getkeyname(int code); extern const char *addreleaseaction(char *s); extern tagval *addreleaseaction(ident *id, int numargs); diff --git a/source/game/gameclient.cpp b/source/game/gameclient.cpp index 2cf3113f..b458608a 100644 --- a/source/game/gameclient.cpp +++ b/source/game/gameclient.cpp @@ -1110,22 +1110,88 @@ namespace game VARP(chatmentions, 0, 1, 1); + static const char *lowerstr(const char *s) + { + char *ret = newstring(s); + for(char *p = ret; *p; ++p) *p = (*p == ' ') ? '_' : cubelower(*p); + return ret; + } + static inline void lowername(gameent *pl, char *buf) + { + int i; + for(i = 0; pl->name[i]; ++i) buf[i] = (pl->name[i] == ' ') ? '_' : cubelower(pl->name[i]); + buf[i] = '\0'; + } + static bool comparenames(gameent *a, gameent *b) + { + static char lower_a[MAXNAMELEN+1], lower_b[MAXNAMELEN+1]; + + lowername(a, lower_a); + lowername(b, lower_b); + + const int val = strcmp(lower_a, lower_b); + if(val < 0) return true; + if(val > 0) return false; + return a < b; + } + const char *completename(const char *start, int len, const char *last, const char *next) + { + vector sorted_players = players; + sorted_players.sort(comparenames); + + const char *start_lower = lowerstr(start); + const char *last_lower = last ? lowerstr(last) : NULL; + const char *next_lower = next ? lowerstr(next) : NULL; + + loopv(sorted_players) + { + gameent *pl = sorted_players[i]; + char name_lower[MAXNAMELEN+1]; + lowername(pl, name_lower); + if(strncmp(name_lower, start_lower, len) == 0 && + (!last || strcmp(name_lower, last_lower) > 0) && (!next || strcmp(name_lower, next_lower) < 0) + ) + { + delete[] start_lower; delete[] last_lower; delete[] next_lower; + return pl->name; + } + } + + delete[] start_lower; delete[] last_lower; delete[] next_lower; + return NULL; + } + bool ischatmention(const char* text) { if (!chatmentions) { return false; } - const char* mention = strstr(text, self->name); - if (mention && mention > text && *(mention - 1) == '@') + + // convert name and text to lowercase and remove spaces from name + static char lowername[MAXNAMELEN+1]; + int i; + for(i = 0; self->name[i+1]; ++i) + { + lowername[i] = isspace(self->name[i]) ? '_' : cubelower(self->name[i]); + } + lowername[i] = '\0'; + char *lowertext = newstring(text); + for(const char *p = text; *p; ++p) lowertext[p - text] = cubelower(*p); + + // check against the lowercased name + const char* mention = strstr(lowertext, lowername); + if (mention && mention > lowertext && *(mention - 1) == '@') { // Checking if a chat message mentions our user name. const char next = *(mention + strlen(self->name)); if (next == '\0' || isspace(next)) { + delete[] lowertext; return true; } } + delete[] lowertext; return false; } diff --git a/source/shared/igame.h b/source/shared/igame.h index d0f430be..90f3f645 100644 --- a/source/shared/igame.h +++ b/source/shared/igame.h @@ -129,6 +129,7 @@ namespace game extern const char *getclientmap(); extern const char *getmapinfo(); extern const char *getscreenshotinfo(); + extern const char *completename(const char *start, int len, const char *last, const char *next); extern int numdynents(const int flags = DYN_PLAYER|DYN_AI); extern int scaletime(int t);