From 4fbe2c31fe87b8e298fd477f4be3de8df1be1456 Mon Sep 17 00:00:00 2001 From: Indev Date: Sun, 23 Nov 2025 01:31:23 +0300 Subject: [PATCH] emotes: change autocompletion behavior to work like console --- src/hu_stuff.c | 49 ++++++++++++---- src/m_textinput.c | 139 +++++++++++++++++++++++++--------------------- src/m_textinput.h | 8 ++- 3 files changed, 121 insertions(+), 75 deletions(-) diff --git a/src/hu_stuff.c b/src/hu_stuff.c index 96a5ca6a7..7ed1bb86e 100644 --- a/src/hu_stuff.c +++ b/src/hu_stuff.c @@ -96,8 +96,7 @@ static boolean headsupactive = false; boolean hu_showscores; // draw rankings static char hu_tick; static tic_t hu_emoteanim = 0; -#define MAXEMOTESUGGESTIONS 8 -static emote_t *emote_suggestions[MAXEMOTESUGGESTIONS] = {0}; +emote_autocomplete_t emote_autocomplete = {0}; //------------------------------------------- // misc vars @@ -1194,6 +1193,7 @@ boolean HU_Responder(event_t *ev) chat_on = true; w_chat_buf[0] = 0; M_TextInputInit(&w_chat, w_chat_buf, sizeof w_chat_buf); + memset(&emote_autocomplete, 0, sizeof(emote_autocomplete)); teamtalk = false; chat_scrollmedown = true; typelines = 1; @@ -1205,6 +1205,7 @@ boolean HU_Responder(event_t *ev) chat_on = true; w_chat_buf[0] = 0; M_TextInputInit(&w_chat, w_chat_buf, sizeof w_chat_buf); + memset(&emote_autocomplete, 0, sizeof(emote_autocomplete)); teamtalk = G_GametypeHasTeams(); // Don't teamtalk if we don't have teams. chat_scrollmedown = true; typelines = 1; @@ -1228,7 +1229,7 @@ boolean HU_Responder(event_t *ev) && !G_ControlBoundToKey(0, gc_talkkey, ev->data1, false)) return false; - M_TextInputHandleEmotes(&w_chat, c, emote_suggestions, MAXEMOTESUGGESTIONS); + M_TextInputHandleEmotes(&w_chat, c, &emote_autocomplete); if (c == KEY_ENTER) { @@ -1775,17 +1776,37 @@ static void HU_DrawChat(void) } } - if (emote_suggestions[0]) + if (emote_autocomplete.emotestart != -1 && (emote_autocomplete.complete[0] || (w_chat.cursor - emote_autocomplete.emotestart) > 0)) { + emote_t *suggest; + int skip = 0; + + const char *complete; + int complete_len; + + if (emote_autocomplete.complete[0]) + { + complete = emote_autocomplete.complete; + complete_len = strlen(complete); + } + else + { + complete = &w_chat_buf[emote_autocomplete.emotestart]; + complete_len = w_chat.cursor - emote_autocomplete.emotestart; + } + // A bit of copy-paste from /pm code :p INT32 suggesty = chaty - charheight - 1; size_t longest_suggestion_length = 0; - for (i = 0; i < MAXEMOTESUGGESTIONS && emote_suggestions[i]; ++i) + while ((suggest = M_FindEmote(complete, complete_len, skip)) != NULL) { - longest_suggestion_length = max(longest_suggestion_length, strlen(emote_suggestions[i]->name)); + longest_suggestion_length = max(longest_suggestion_length, strlen(suggest->name)); + ++skip; } + skip = 0; + #ifdef NETSPLITSCREEN if (splitscreen) { @@ -1797,11 +1818,19 @@ static void HU_DrawChat(void) #endif suggesty -= (cv_kartspeedometer.value ? 16 : 0); - for (i = 0; i < MAXEMOTESUGGESTIONS && emote_suggestions[i]; ++i) + while ((suggest = M_FindEmote(complete, complete_len, skip)) != NULL) { - V_DrawFillConsoleMap(chatx + boxw + 2, suggesty - (7*i), (longest_suggestion_length+2)*4 + EMOTEWIDTH, 6, 239|V_SNAPTOBOTTOM|V_SNAPTOLEFT); - V_DrawSmallString(chatx + boxw + 4 + EMOTEWIDTH, suggesty - (7*i), V_SNAPTOBOTTOM|V_SNAPTOLEFT|V_ALLOWLOWERCASE, va(":%s:", emote_suggestions[i]->name)); - HU_DrawEmote(chatx + boxw + 2, suggesty - i*7, emote_suggestions[i], V_SNAPTOBOTTOM|V_SNAPTOLEFT); + V_DrawFillConsoleMap(chatx + boxw + 2, suggesty - (7*skip), (longest_suggestion_length+2)*4 + EMOTEWIDTH, 6, V_SNAPTOBOTTOM|V_SNAPTOLEFT); + + // Highlight currently suggested emote + int hlflag = 0; + if (skip == emote_autocomplete.skip && emote_autocomplete.complete[0]) + hlflag = V_YELLOWMAP; + + V_DrawSmallString(chatx + boxw + 4 + EMOTEWIDTH, suggesty - (7*skip), hlflag|V_SNAPTOBOTTOM|V_SNAPTOLEFT|V_ALLOWLOWERCASE, va(":%s:", suggest->name)); + HU_DrawEmote(chatx + boxw + 2, suggesty - skip*7, suggest, V_SNAPTOBOTTOM|V_SNAPTOLEFT); + + ++skip; } } diff --git a/src/m_textinput.c b/src/m_textinput.c index 6468ee0b7..8813d45e1 100644 --- a/src/m_textinput.c +++ b/src/m_textinput.c @@ -401,21 +401,13 @@ boolean M_TextInputHandle(textinput_t *input, INT32 key) return M_TextInputHandleBase(input, key, false); } -boolean M_TextInputHandleEmotes(textinput_t *input, INT32 key, emote_t *suggestions[], int maxsuggestions) +static int M_TextInputEmoteStart(textinput_t *input) { - boolean ret = M_TextInputHandleBase(input, key, true); + if (input->cursor == 0) + return -1; - // After this handled key we ended up inside an emote, lets fix that - if (M_TextInputCheckEmote(input)) - M_TextInputToWordEnd(input, !shiftdown); + int emotestart = -1; - // Always clear the first entry - suggestions[0] = NULL; - - // For autocomplete - int emotestart = 0; - - // Try find suggestions for emote names for (int i = input->cursor-1; i >= 0 && input->cursor-i <= MAXEMOTENAME; --i) { // Space can't be part of emote name @@ -426,71 +418,90 @@ boolean M_TextInputHandleEmotes(textinput_t *input, INT32 key, emote_t *suggesti if (input->buffer[i] == ':') { emotestart = i+1; - // ...But only if we typed at least something - if ((int)input->cursor-(i-1) < 3) - break; - - for (int skip = 0; skip < maxsuggestions; ++skip) - { - suggestions[skip] = M_FindEmote(input->buffer+i+1, input->cursor-i-1, skip); - - // No more suggestions - if (!suggestions[skip]) - break; - } - - // In any case, we found what we wanted, can exit the loop now break; } } - const boolean single_suggestion = suggestions[0] && (maxsuggestions == 1 || !suggestions[1]); + return emotestart; +} - // If we suggest emotes, try autocomplete - if ((key == '\t' && suggestions[0]) || (key == KEY_ENTER && single_suggestion)) +static void M_TextInputCompleteEmote(textinput_t *input, emote_autocomplete_t *autocomplete) +{ + if (input->cursor == 0) + return; + + // If we're in "autocomplete" state, delete ':' for latest autocompleted emote + if (autocomplete->complete[0]) { - int pos = 0; - const int insertpos = (input->cursor-emotestart); // Only insert after that index (including it) - boolean autocomplete = true; - - while (autocomplete) + if (input->buffer[input->cursor-1] == ':') { - // Check if current character matches for all suggestions - char c = suggestions[0]->name[pos]; - - // End of string reached - if (!c) - break; - - for (int i = 1; i < maxsuggestions && suggestions[i]; ++i) - { - if (suggestions[i]->name[pos] != c) - { - autocomplete = false; - break; - } - } - - ++pos; - - if (autocomplete && pos > insertpos) - M_TextInputAddChar(input, c); + M_TextInputDelChar(input); } - // This was the only suggestion, finish autocomplete with a ':' and clear suggestions - if (single_suggestion) + autocomplete->skip++; + } + else + { + // Try to find emote to autocomplete + + int emotestart = autocomplete->emotestart; + + // No emote - no autocomplete + if (emotestart == -1 || (size_t)emotestart == input->cursor) + return; + + strlcpy(autocomplete->complete, &input->buffer[emotestart], input->cursor-emotestart+1); + } + + emote_t *completed = M_FindEmote(autocomplete->complete, strlen(autocomplete->complete), autocomplete->skip); + + // We hit end of list of suggested emotes + if (completed == NULL) + { + autocomplete->skip = 0; + completed = M_FindEmote(autocomplete->complete, strlen(autocomplete->complete), autocomplete->skip); + + // NULL again, list was empty to begin with + if (completed == NULL) { - memcpy(&input->buffer[emotestart], suggestions[0]->name, strlen(suggestions[0]->name)); - M_TextInputAddChar(input, ':'); - suggestions[0] = NULL; - } - else if (pos > emotestart) - { - // Otherwise make sure beginning of emote matches - memcpy(&input->buffer[emotestart], suggestions[0]->name, pos-emotestart); + // Clear autocompletion state + autocomplete->complete[0] = 0; + return; } } + // Erase previously typed emote. Not really efficent but too lazy to count chars and do M_TextInputDel() + while (input->buffer[input->cursor-1] != ':') + { + M_TextInputDelChar(input); + } + + M_TextInputAddString(input, completed->name); + M_TextInputAddChar(input, ':'); +} + +boolean M_TextInputHandleEmotes(textinput_t *input, INT32 key, emote_autocomplete_t *autocomplete) +{ + boolean ret = M_TextInputHandleBase(input, key, true); + + // After this handled key we ended up inside an emote, lets fix that + if (M_TextInputCheckEmote(input)) + M_TextInputToWordEnd(input, !shiftdown); + + // Always update it + autocomplete->emotestart = M_TextInputEmoteStart(input); + + if (key == '\t' || (autocomplete->complete[0] == 0 && key == KEY_ENTER)) + { + M_TextInputCompleteEmote(input, autocomplete); + } + else + { + // Reset state just in case + autocomplete->complete[0] = 0; + autocomplete->skip = 0; + } + return ret; } diff --git a/src/m_textinput.h b/src/m_textinput.h index 1cc1c5005..7ddb0bf65 100644 --- a/src/m_textinput.h +++ b/src/m_textinput.h @@ -15,6 +15,12 @@ typedef struct textinput_s { unsigned skull; } textinput_t; +typedef struct emote_autocomplete_s { + char complete[MAXEMOTENAME+1]; + int skip; + int emotestart; +} emote_autocomplete_t; + void M_TextInputInit(textinput_t *input, char *buffer, size_t buffer_size); void M_TextInputClear(textinput_t *input); @@ -25,7 +31,7 @@ boolean M_TextInputHandle(textinput_t *input, INT32 key); // Does everything same as M_TextInputHandle, but also handles emotes in input string // If currently typing something that looks like an existing emote, return suggestions for it -boolean M_TextInputHandleEmotes(textinput_t *input, INT32 key, emote_t *suggestions[], int maxsuggestions); +boolean M_TextInputHandleEmotes(textinput_t *input, INT32 key, emote_autocomplete_t *autocomplete); #define M_DrawTextInput(x, y, input, flags) M_DrawTextInputScroll((x), (y), (input), (flags), ((input)->buffer_size-1)*8) void M_DrawTextInputScroll(INT32 x, INT32 y, textinput_t *input, INT32 flags, INT32 MAXINPUTWIDTH);