diff --git a/src/Sourcefile b/src/Sourcefile index 1c01a18cc..010dffeee 100644 --- a/src/Sourcefile +++ b/src/Sourcefile @@ -37,6 +37,7 @@ m_menu.c m_textinput.c m_memcpy.c m_misc.cpp +m_emotes.cpp m_perfstats.c m_random.c m_queue.c diff --git a/src/d_main.cpp b/src/d_main.cpp index 86bc241bf..d7eac7942 100644 --- a/src/d_main.cpp +++ b/src/d_main.cpp @@ -38,6 +38,7 @@ #include "f_finale.h" #include "g_game.h" #include "hu_stuff.h" +#include "m_emotes.h" #include "i_sound.h" #include "i_system.h" #include "i_time.h" @@ -1799,6 +1800,8 @@ void D_SRB2Main(void) S_InitMusicDefs(); + M_InitEmotes(); + CONS_Printf("ST_Init(): Init status bar.\n"); ST_Init(); CON_SetLoadingProgress(LOADED_STINIT); diff --git a/src/d_netcmd.c b/src/d_netcmd.c index 02686c592..7fb93208f 100644 --- a/src/d_netcmd.c +++ b/src/d_netcmd.c @@ -21,6 +21,7 @@ #include "i_system.h" #include "g_game.h" #include "hu_stuff.h" +#include "m_emotes.h" #include "g_input.h" #include "m_menu.h" #include "p_mobj.h" @@ -1104,6 +1105,8 @@ void D_RegisterClientCommands(void) // HUD CV_RegisterVar(&cv_itemfinder); + CV_RegisterVar(&cv_emotes); + // time attack ghost options are also saved to config CV_RegisterVar(&cv_ghost_besttime); CV_RegisterVar(&cv_ghost_bestlap); diff --git a/src/hu_stuff.c b/src/hu_stuff.c index 5c74c5691..7cbc8ac63 100644 --- a/src/hu_stuff.c +++ b/src/hu_stuff.c @@ -19,6 +19,7 @@ #include "m_menu.h" // gametype_cons_t #include "m_cond.h" // emblems #include "m_misc.h" // word jumping +#include "m_emotes.h" #include "d_clisrv.h" @@ -94,6 +95,9 @@ static textinput_t w_chat; 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}; //------------------------------------------- // misc vars @@ -983,6 +987,7 @@ void HU_Ticker(void) return; hu_tick++; + hu_emoteanim++; hu_tick &= 7; // currently only to blink chat input cursor if (G_PlayerInputDown(0, gc_scores, false)) @@ -1227,7 +1232,7 @@ boolean HU_Responder(event_t *ev) && !G_ControlBoundToKey(0, gc_talkkey, ev->data1, false)) return false; - M_TextInputHandle(&w_chat, c); + M_TextInputHandleEmotes(&w_chat, c, emote_suggestions, MAXEMOTESUGGESTIONS); if (c == KEY_ENTER) { @@ -1269,6 +1274,8 @@ boolean HU_Responder(event_t *ev) // HEADS UP DRAWING //====================================================================== +#define HU_DrawEmote(x, y, emote, flags) M_DrawEmote((x), (y), (emote), hu_emoteanim, (flags)) + // Precompile a wordwrapped string to any given width. // This is a muuuch better method than V_WORDWRAP. // again stolen and modified a bit from video.c, don't mind me, will need to rearrange this one day. @@ -1280,6 +1287,8 @@ static char *CHAT_WordWrap(INT32 x, INT32 w, INT32 option, const char *string) size_t slen; char *newstring = Z_StrDup(string); INT32 spacewidth = (vid.width < 640) ? 8 : 4, charwidth = (vid.width < 640) ? 8 : 4; + emote_t *emote = NULL; + int emotelen = 0; slen = strlen(string); x = 0; @@ -1301,7 +1310,12 @@ static char *CHAT_WordWrap(INT32 x, INT32 w, INT32 option, const char *string) c = toupper(c); c -= HU_FONTSTART; - if (c < 0 || c >= HU_FONTSIZE || !fontv[HU_FONT].font[c]) + if ((emote = M_VerifyEmote(string+i, &emotelen))) + { + chw = EMOTEWIDTH; + i += emotelen-1; // Will be incremented on next loop iteration, hence -1 + } + else if (c < 0 || c >= HU_FONTSIZE || !fontv[HU_FONT].font[c]) { chw = spacewidth; lastusablespace = i; @@ -1323,7 +1337,6 @@ static char *CHAT_WordWrap(INT32 x, INT32 w, INT32 option, const char *string) return newstring; } - // 30/7/18: chaty is now the distance at which the lowest point of the chat will be drawn if that makes any sense. INT16 chatx = 13, chaty = 169; // let's use this as our coordinates @@ -1356,6 +1369,8 @@ static void HU_drawMiniChat(void) char *msg = CHAT_WordWrap(x+2, boxw-(charwidth*2), V_SNAPTOBOTTOM|V_SNAPTOLEFT|V_ALLOWLOWERCASE, chat_mini[i-1]); size_t j = 0; INT32 linescount = 0; + emote_t *emote = NULL; + int emotelen = 0; while(msg[j]) // iterate through msg { @@ -1380,6 +1395,11 @@ static void HU_drawMiniChat(void) ++j; } + else if ((emote = M_VerifyEmote(msg+j, &emotelen))) + { + dx += EMOTEWIDTH - charwidth; + j += emotelen; + } else { j++; @@ -1428,6 +1448,8 @@ static void HU_drawMiniChat(void) size_t j = 0; char *msg = CHAT_WordWrap(x+2, boxw-(charwidth*2), V_SNAPTOBOTTOM|V_SNAPTOLEFT|V_ALLOWLOWERCASE, chat_mini[i]); // get the current message, and word wrap it. UINT8 *colormap = NULL; + emote_t *emote = NULL; + int emotelen = 0; while(msg[j]) // iterate through msg { @@ -1454,6 +1476,16 @@ static void HU_drawMiniChat(void) ++j; } + else if ((emote = M_VerifyEmote(msg+j, &emotelen))) + { + if (cv_chatbacktint.value) // on request of wolfy + V_DrawFillConsoleMap(x + dx + 2, y+dy, EMOTEWIDTH, charheight, 239|V_SNAPTOBOTTOM|V_SNAPTOLEFT); + + HU_DrawEmote(x+dx+2, y+dy, emote, V_SNAPTOBOTTOM|V_SNAPTOLEFT|transflag); + dx += EMOTEWIDTH - charwidth; + + j += emotelen; + } else { if (cv_chatbacktint.value) // on request of wolfy @@ -1534,6 +1566,9 @@ static void HU_drawChatLog(INT32 offset) INT32 j = 0; char *msg = CHAT_WordWrap(x+2, boxw-(charwidth*2), V_SNAPTOBOTTOM|V_SNAPTOLEFT|V_ALLOWLOWERCASE, chat_log[i]); // get the current message, and word wrap it. UINT8 *colormap = NULL; + emote_t *emote = NULL; + int emotelen = 0; + while(msg[j]) // iterate through msg { if (msg[j] < HU_FONTSTART) // don't draw @@ -1555,6 +1590,15 @@ static void HU_drawChatLog(INT32 offset) ++j; } + else if ((emote = M_VerifyEmote(msg+j, &emotelen))) + { + if ((y+dy+2 >= chat_topy) && (y+dy < (chat_bottomy))) + { + HU_DrawEmote(x+dx+2, y+dy, emote, V_SNAPTOBOTTOM|V_SNAPTOLEFT); + dx += EMOTEWIDTH - charwidth; + } + j += emotelen; + } else { if ((y+dy+2 >= chat_topy) && (y+dy < (chat_bottomy))) @@ -1681,7 +1725,7 @@ static void HU_DrawChat(void) typelines = 1; if ((w_chat.cursor == 0 || w_chat.length == 0) && hu_tick < 4) - V_DrawChatCharacter(chatx+2+c+charwidth*w_chat.cursor, y+1, '_'|V_SNAPTOBOTTOM|V_SNAPTOLEFT|t, !cv_menucaps.value, NULL); + V_DrawChatCharacter(chatx+2+c, y+1, '_'|V_SNAPTOBOTTOM|V_SNAPTOLEFT|t, !cv_menucaps.value, NULL); if (w_chat.select != w_chat.cursor) { @@ -1692,29 +1736,40 @@ static void HU_DrawChat(void) while (w_chat_buf[i]) { boolean skippedline = false; - if (w_chat.cursor == (i+1)) + emote_t *emote = NULL; + int emotelen = 0; + int drawwidth = charwidth; // if we've drawn emote, selection needs to account for that + + if ((emote = M_VerifyEmote(w_chat_buf+i, &emotelen))) + { + HU_DrawEmote(chatx + c + 2, y-1, emote, V_SNAPTOBOTTOM|V_SNAPTOLEFT|t); + c += EMOTEWIDTH - charwidth; + i += emotelen-1; + drawwidth = EMOTEWIDTH; + } + else if (w_chat_buf[i] >= HU_FONTSTART) //Hurdler: isn't it better like that? + V_DrawChatCharacter(chatx + c + 2, y, w_chat_buf[i] | V_SNAPTOBOTTOM|V_SNAPTOLEFT | t, !cv_menucaps.value, NULL); + + // Draw selection + if (i >= select_start && i < select_end) + V_DrawFill(chatx + c + 2 - (drawwidth-charwidth), y-1, drawwidth, charheight, 103|V_TRANSLUCENT|V_SNAPTOBOTTOM|V_SNAPTOLEFT|t); + + ++i; + + if (w_chat.cursor == i) { INT32 cursorx = (c+charwidth < boxw-charwidth) ? (chatx + 2 + c+charwidth) : (chatx+1); // we may have to go down. INT32 cursory = (cursorx != chatx+1) ? (y) : (y+charheight); if (hu_tick < 4) V_DrawChatCharacter(cursorx, cursory+1, '_' |V_SNAPTOBOTTOM|V_SNAPTOLEFT|t, true, NULL); - if (cursorx == chatx+1 && saylen == i) // a weirdo hack + if (cursorx == chatx+1 && saylen == i-1) // a weirdo hack { typelines += 1; skippedline = true; } } - //Hurdler: isn't it better like that? - if (w_chat_buf[i] >= HU_FONTSTART) - V_DrawChatCharacter(chatx + c + 2, y, w_chat_buf[i] | V_SNAPTOBOTTOM|V_SNAPTOLEFT | t, !cv_menucaps.value, NULL); - - // Draw selection - if (i >= select_start && i < select_end) - V_DrawFill(chatx + c + 2, y-1, charwidth, charheight, 103|V_TRANSLUCENT|V_SNAPTOBOTTOM|V_SNAPTOLEFT|t); - ++i; - c += charwidth; if (c > boxw-(charwidth*2) && !skippedline) { @@ -1724,6 +1779,36 @@ static void HU_DrawChat(void) } } + if (emote_suggestions[0]) + { + // 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) + { + longest_suggestion_length = max(longest_suggestion_length, strlen(emote_suggestions[i]->name)); + } + +#ifdef NETSPLITSCREEN + if (splitscreen) + { + suggesty -= BASEVIDHEIGHT/2; + if (splitscreen > 1) + suggesty += 16; + } + else +#endif + suggesty -= (cv_kartspeedometer.value ? 16 : 0); + + for (i = 0; i < MAXEMOTESUGGESTIONS && emote_suggestions[i]; ++i) + { + 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); + } + } + // handle /pm list. It's messy, horrible and I don't care. if (strnicmp(w_chat_buf, "/pm", 3) == 0 && vid.width >= 400 && !teamtalk) // 320x200 unsupported kthxbai { diff --git a/src/m_emotes.cpp b/src/m_emotes.cpp new file mode 100644 index 000000000..a827fef1d --- /dev/null +++ b/src/m_emotes.cpp @@ -0,0 +1,262 @@ +#include +#include +#include +#include + +#include "m_emotes.h" + +extern "C" { +#include "w_wad.h" +#include "z_zone.h" +#include "hu_stuff.h" +#include "v_video.h" +} + +consvar_t cv_emotes = CVAR_INIT ("emotes", "On", CV_SAVE, CV_OnOff, NULL); + +static std::map emotes; + +// TODO - Would be really nice to generalize soc-like file parsing and not reimplement it 100 times :))))) +void M_LoadEmotes(UINT16 wadnum) +{ + UINT16 lumpnum = W_CheckNumForNamePwad("EMOTES", wadnum, 0); + + if (lumpnum == INT16_MAX) + return; + + char *lump = static_cast(W_CacheLumpNumPwad(wadnum, lumpnum, PU_CACHE)); + size_t size = W_LumpLengthPwad(wadnum, lumpnum); + + std::istringstream file(std::string(lump, size)); + std::string line; + + emote_t *emote = nullptr; + std::string emote_name; + + // Just smol lambda to remove all the trailing chars we might not want + auto trim = [](std::string &str) { + str.erase(0, str.find_first_not_of(" \t\n\r\f\v")); + str.erase(str.find_last_not_of(" \t\n\r\f\v")+1); + }; + + int numemotes = 0; + int linenum = 0; + + while (std::getline(file, line)) + { + ++linenum; + + trim(line); + + if (line.empty()) + continue; + + size_t split = line.find('='); + + // Didn't find the =, means we're about to parse `Emote ` + if (split == line.npos) + { + split = line.find(' '); + + if (split == line.npos) + { + CONS_Alert(CONS_WARNING, "EMOTES: Unrecognized line. (file %s, line %d)\n", wadfiles[wadnum]->filename, linenum); + continue; + } + + if (strnicmp(line.c_str(), "emote", 5)) + { + CONS_Alert(CONS_WARNING, "EMOTES: 'Emote' expected. (file %s, line %d)\n", wadfiles[wadnum]->filename, linenum); + continue; + } + + emote_name = line.substr(split+1); + + if (emote_name.find_first_of(" \t\n\r\f\v:") != emote_name.npos) + { + CONS_Alert(CONS_WARNING, "EMOTES: Emote name cannot contain spaces or ':' symbols. (file %s, line %d)\n", wadfiles[wadnum]->filename, linenum); + continue; + } + + if (emote_name.size() > MAXEMOTENAME) + { + CONS_Alert(CONS_WARNING, "EMOTES: Emote name is too long, truncating. (file %s, line %d)\n", wadfiles[wadnum]->filename, linenum); + emote_name = emote_name.substr(0, MAXEMOTENAME); + } + + emote = &emotes[emote_name]; + strlcpy(emote->name, emote_name.c_str(), MAXEMOTENAME); + + emote->timeperframe = 1; + emote->numframes = 0; + std::memset(emote->frames, 0, sizeof(emote->frames)); + + ++numemotes; + } + else if (emote) + { + std::string field = line.substr(0, split); + std::string value = line.substr(split+1); + + trim(field); + trim(value); + + if (stricmp(field.c_str(), "frames") == 0) + { + UINT8 numframes = 0; + + auto copy_frame = [&] { + if (numframes == MAXEMOTEFRAMES) + { + CONS_Alert(CONS_WARNING, "EMOTES: Too many frames. (file %s, line %d)", wadfiles[wadnum]->filename, linenum); + return false; + } + + size_t list_split = value.find(','); + std::string framelumpname = value.substr(0, list_split); + trim(framelumpname); + + if (list_split == value.npos) + value = ""; + else + value = value.substr(list_split+1, value.npos); + + // End of list of frames + if (framelumpname.size() == 0) + return false; + + if (framelumpname.size() > 8) + { + CONS_Alert(CONS_WARNING, "EMOTES: Frame %d name is too long. (file %s, line %d)", numframes, wadfiles[wadnum]->filename, linenum); + framelumpname = framelumpname.substr(0, 8); + } + + std::strncpy(emote->frames[numframes], framelumpname.c_str(), framelumpname.size()+1); + + ++numframes; + + return true; + }; + + while (copy_frame()) {} + + if (numframes == 0) + { + CONS_Alert(CONS_WARNING, "EMOTES: Expected list of frames. (file %s, line %d)", wadfiles[wadnum]->filename, linenum); + } + + emote->numframes = numframes; + } + else if (stricmp(field.c_str(), "timeperframe") == 0) + { + emote->timeperframe = std::stoi(value); + + if (emote->timeperframe <= 0) + { + emote->timeperframe = 1; + CONS_Alert(CONS_WARNING, "EMOTES: Bad value for 'timeperframe'. (file %s, line %d)", wadfiles[wadnum]->filename, linenum); + } + } + else + { + CONS_Alert(CONS_WARNING, "EMOTES: Unrecognized field '%s'. (file %s, line %d)", field.c_str(), wadfiles[wadnum]->filename, linenum); + } + } + else + { + CONS_Alert(CONS_WARNING, "EMOTES: Unrecognized line. (file %s, line %d)\n", wadfiles[wadnum]->filename, linenum); + } + } + + CONS_Printf("Added %d emotes\n", numemotes); +} + +void M_InitEmotes(void) +{ + UINT16 i; + for (i = 0; i < numwadfiles; i++) + M_LoadEmotes(i); +} + +emote_t *M_FindEmote(const char *name, int len, int skip) +{ + if (!cv_emotes.value) + return nullptr; + + char query[MAXEMOTENAME+1] = {0}; + std::strncpy(query, name, std::min(len, MAXEMOTENAME)); + + for (auto &pair: emotes) + { + if (pair.first.rfind(query, 0, std::min(len, MAXEMOTENAME)) != 0) + continue; + + if (skip > 0) + { + --skip; + continue; + } + + return &pair.second; + } + + return nullptr; +} + +emote_t *M_VerifyEmote(const char *name, int *emotelen) +{ + if (*name != ':') + return nullptr; + + if (!cv_emotes.value) + return nullptr; + + const char *p = name+1; // skip ':' + emote_t *match = nullptr; + + while (*p && (p - name) < MAXEMOTENAME) + { + if (*p == ':') + { + std::string checkname = std::string(name+1, (p-name)-1); + auto it = emotes.find(checkname); + + if (it != emotes.end()) + match = &it->second; + + break; + } + + ++p; + } + + if (match && emotelen) + *emotelen = p-name+1; + + return match; +} + +void M_DrawEmote(INT32 x, INT32 y, emote_t *emote, tic_t anim, INT32 flags) +{ + if (emote->numframes == 0) + return; + + const char *lumpname = emote->frames[(anim/emote->timeperframe) % emote->numframes]; + patch_t *emotepatch = (patch_t*)W_CachePatchName(lumpname, PU_CACHE); + + const int CHARHEIGHT = 6; + + fixed_t scale = FRACUNIT; + + x *= FRACUNIT; + y *= FRACUNIT; + + if (emotepatch->width > EMOTEWIDTH) + scale = (FRACUNIT/emotepatch->width)*EMOTEWIDTH; + else if (emotepatch->width < EMOTEWIDTH) + x += (EMOTEWIDTH-emotepatch->width)*FRACUNIT/2; + + y -= (scale*emotepatch->height-CHARHEIGHT*FRACUNIT)/2; + + V_DrawFixedPatch(x, y, scale, flags, emotepatch, NULL); +} diff --git a/src/m_emotes.h b/src/m_emotes.h new file mode 100644 index 000000000..783c6ec17 --- /dev/null +++ b/src/m_emotes.h @@ -0,0 +1,48 @@ +#ifndef __M_EMOTES__ +#define __M_EMOTES__ + +#ifdef __cplusplus +extern "C" { +#endif + +#include "doomdef.h" +#include "command.h" + +extern consvar_t cv_emotes; + +#define MAXEMOTENAME 32 +#define MAXEMOTEFRAMES 32 +#define EMOTEWIDTH 6 + +typedef struct emote_s { + char name[MAXEMOTENAME+1]; + char frames[MAXEMOTEFRAMES][9]; + UINT8 numframes; + tic_t timeperframe; +} emote_t; + +// Reads and adds emotes from EMOTES lump, with syntax similar to soc +void M_LoadEmotes(UINT16 wadnum); + +// Load emotes from all base wads +void M_InitEmotes(void); + +// Finds first matching emote, ignoring first few matches +emote_t *M_FindEmote(const char *name, int len, int skips); + +// If first character is not ':', instantly returns null +// Starts from ':' character, goes until it finds ':' or end of string +// If end of string is found, or closing ':' is found but resulting emote name doesn't exist, +// it returns null, otherwise it returns emote and stores into *emotelen how much characters +// needs to be skipped to get past emote name +emote_t *M_VerifyEmote(const char *name, int *emotelen); + +// Draw the emote, anim should be some kind of timer ticking every game tic +// for animated emotes +void M_DrawEmote(INT32 x, INT32 y, emote_t *emote, tic_t anim, INT32 flags); + +#ifdef __cplusplus +} // extern "C" +#endif + +#endif // __M_EMOTES__ diff --git a/src/m_menu.c b/src/m_menu.c index 6a5863ad5..573442ded 100644 --- a/src/m_menu.c +++ b/src/m_menu.c @@ -2229,23 +2229,6 @@ void M_DrawTextBox(INT32 x, INT32 y, INT32 width, INT32 boxlines) V_DrawFill(x+5, y+5, width*8+6, boxlines*8+6, 159); } -static void M_DrawTextInput(INT32 x, INT32 y, INT32 flags, textinput_t *input) -{ - // draw text cursor for name - if (skullAnimCounter < 4) // blink cursor - V_DrawCharacter(x+V_SubStringWidth(input->buffer, input->cursor, flags|V_ALLOWLOWERCASE), y+3, '_'|(flags & ~(V_FLIP|V_PARAMMASK)), false); - - // draw selection - if (input->select != input->cursor) - { - size_t start = min(input->select, input->cursor); - size_t end = max(input->select, input->cursor); - size_t len = end - start; - INT32 startx = V_SubStringWidth(input->buffer, start, flags|V_ALLOWLOWERCASE); - V_DrawFill(x+startx, y, V_SubStringWidth(input->buffer+start, len, V_ALLOWLOWERCASE), 8, (flags & ~(V_ALPHAMASK|V_PARAMMASK))|103|V_TRANSLUCENT); - } -} - // horizontally centered text static void M_CentreText(INT32 y, const char *string) { @@ -2353,7 +2336,7 @@ static void M_DrawRightString(menuitem_t *item, INT16 x, INT16 y, INT32 vflags, M_DrawTextBox(x + xofs, y + yofs, w, 1); V_DrawString(x + xofs + 8, y + yofs + 8, vflags|V_ALLOWLOWERCASE, cv->string); if (selected) - M_DrawTextInput(x + xofs + 8, y + yofs + 8, vflags|V_ALLOWLOWERCASE, &menuinput); + M_DrawTextInput(x + xofs + 8, y + yofs + 8, &menuinput, vflags|V_ALLOWLOWERCASE); } else if (item->status & IT_SLIDER) { diff --git a/src/m_textinput.c b/src/m_textinput.c index 8d2c6b9e3..e90410a0c 100644 --- a/src/m_textinput.c +++ b/src/m_textinput.c @@ -1,4 +1,7 @@ #include "m_textinput.h" +#include "m_menu.h" // MAXSTRINGLENGTH +#include "v_video.h" +#include "r_main.h" // renderisnewtic #include "i_system.h" #include "keys.h" #include "console.h" @@ -118,11 +121,98 @@ static void M_TextInputToWordBegin(textinput_t *input, boolean move_sel) if (move_sel) input->select = input->cursor; } +static void M_TextInputPaste(textinput_t *input) +{ + const char *paste = I_ClipboardPaste(); + if (input->select != input->cursor) + M_TextInputDelSelection(input); + if (paste != NULL) + M_TextInputAddString(input, paste); +} + +static void M_TextInputLeft(textinput_t *input, boolean emotes) +{ + // Just do it simple way if possible + if (!emotes || input->cursor < 2 || input->buffer[input->cursor-1] != ':') + { + if (input->cursor != 0) + --input->cursor; + return; + } + + // Otherwise we need to check if we have to skip emote + size_t start = input->cursor-2; + + while (input->buffer[start] != ':') + { + // We reached start of string and didn't find ':' symbol, thats definitely not an emote + // so just do it simple way + if (start == 0) + { + M_TextInputLeft(input, false); + return; + } + + --start; + } + + int emotelen = 0; + if (M_VerifyEmote(input->buffer+start, &emotelen)) + input->cursor -= emotelen; // also skip the : + else + M_TextInputLeft(input, false); // Not a valid emote, do the simple thing +} + +static void M_TextInputRight(textinput_t *input, boolean emotes) +{ + // Just do it simple way if possible + if (!emotes || input->cursor == input->length || input->buffer[input->cursor] != ':') + { + if (input->cursor < input->length) + ++input->cursor; + return; + } + + int emotelen = 0; + if (M_VerifyEmote(input->buffer+input->cursor, &emotelen)) + input->cursor += emotelen; // Also skip the : + else + M_TextInputRight(input, false); // Not a valid emote, do the simple thing +} + +// Check if we're inside a valid emote +static boolean M_TextInputCheckEmote(textinput_t *input) +{ + int start = input->cursor; + + // Definitely not an emote + if (start == 0) + return false; + + // Might be closing :, need to skip it just in case + if (input->buffer[start] == ':') + --start; + + while (input->buffer[start] != ':') + { + // No ':' found, definitely not inside emote + if (start == 0) + return false; + + --start; + } + + // Found what may be emote start, now we can check + return M_VerifyEmote(input->buffer+start, NULL) != NULL; +} + void M_TextInputInit(textinput_t *input, char *buffer, size_t buffer_size) { input->buffer = buffer; input->buffer_size = buffer_size; + input->skull = 0; + M_TextInputClear(input); } @@ -131,6 +221,8 @@ void M_TextInputClear(textinput_t *input) input->cursor = 0; input->select = 0; input->length = 0; + + input->buffer[0] = 0; } void M_TextInputSetString(textinput_t *input, const char *c) @@ -140,13 +232,19 @@ void M_TextInputSetString(textinput_t *input, const char *c) input->cursor = input->select = input->length = strlen(c); } -boolean M_TextInputHandle(textinput_t *input, INT32 key) +static boolean M_TextInputHandleBase(textinput_t *input, INT32 key, boolean emotes) { if (key == KEY_LSHIFT || key == KEY_RSHIFT - || key == KEY_LCTRL || key == KEY_RCTRL - || key == KEY_LALT || key == KEY_RALT) + || key == KEY_LCTRL || key == KEY_RCTRL + || key == KEY_LALT || key == KEY_RALT) return false; + if (shiftdown && key == KEY_INS) + { + M_TextInputPaste(input); + return true; + } + //if ((cv_keyboardlayout.value != 3 && ctrldown) || (cv_keyboardlayout.value == 3 && ctrldown && !altdown)) if (ctrldown) { @@ -169,11 +267,7 @@ boolean M_TextInputHandle(textinput_t *input, INT32 key) } else if (key == 'v' || key == 'V') { - const char *paste = I_ClipboardPaste(); - if (input->select != input->cursor) - M_TextInputDelSelection(input); - if (paste != NULL) - M_TextInputAddString(input, paste); + M_TextInputPaste(input); return true; } else if (key == 'w' || key == 'W') @@ -224,16 +318,14 @@ boolean M_TextInputHandle(textinput_t *input, INT32 key) if (key == KEY_LEFTARROW) { - if (input->cursor != 0) - --input->cursor; + M_TextInputLeft(input, emotes); if (!shiftdown) input->select = input->cursor; return true; } else if (key == KEY_RIGHTARROW) { - if (input->cursor < input->length) - ++input->cursor; + M_TextInputRight(input, emotes); if (!shiftdown) input->select = input->cursor; return true; @@ -280,29 +372,181 @@ boolean M_TextInputHandle(textinput_t *input, INT32 key) if (key >= KEY_KEYPAD7 && key <= KEY_KPADDEL) { char keypad_translation[] = {'7','8','9','-', - '4','5','6','+', - '1','2','3', - '0','.'}; + '4','5','6','+', + '1','2','3', + '0','.'}; - key = keypad_translation[key - KEY_KEYPAD7]; + key = keypad_translation[key - KEY_KEYPAD7]; } else if (key == KEY_KPADSLASH) key = '/'; // same capslock code as hu_stuff.c's HU_responder. Check there for details. - key = CON_ShiftChar(key); + key = /*cv_keyboardlayout.value == 3 ? CON_ShitAndAltGrChar(key) : */CON_ShiftChar(key); // enter a char into the command prompt if (key < 32 || key > 127) return false; - // add key to cmd line here - if (key >= 'A' && key <= 'Z' && !(shiftdown ^ capslock)) //this is only really necessary for dedicated servers - key = key + 'a' - 'A'; - if (input->select != input->cursor) M_TextInputDelSelection(input); M_TextInputAddChar(input, key); return true; } + +// Just an alias, more or less +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) +{ + 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 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 + if (isspace(input->buffer[i])) + break; + + // Found a :, try suggest emotes + 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; + } + } + + // If we suggest emotes, try autocomplete + if (key == '\t' && suggestions[0]) + { + int pos = (input->cursor-emotestart); + boolean autocomplete = true; + + while (autocomplete) + { + // 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) + M_TextInputAddChar(input, c); + } + + // This was the only suggestion, finish autocomplete with a ':' and clear suggestions + if (maxsuggestions == 1 || !suggestions[1]) + { + M_TextInputAddChar(input, ':'); + suggestions[0] = NULL; + } + } + + return ret; +} + +void M_DrawTextInputScroll(INT32 x, INT32 y, textinput_t *input, INT32 flags, INT32 MAXINPUTWIDTH) +{ + if (renderisnewtic) + input->skull++; + input->skull %= 8; + + char nametodraw[MAXSTRINGLENGTH*2+1] = {0}; + + size_t drawstart = 0; + size_t drawend = 0; // Only used for selection + + INT32 skullx = x; + + while (V_SubStringWidth(input->buffer+drawstart, input->cursor-drawstart, V_ALLOWLOWERCASE) > MAXINPUTWIDTH) + ++drawstart; + + size_t drawlength = V_SubStringLengthToFit(input->buffer+drawstart, MAXINPUTWIDTH+8, V_ALLOWLOWERCASE)+1; + drawend = drawstart + drawlength; + + memcpy(nametodraw, input->buffer+drawstart, drawlength); + + if (input->length) + skullx += V_SubStringWidth(nametodraw, input->cursor-drawstart, V_ALLOWLOWERCASE); + + V_DrawString(x, y, V_ALLOWLOWERCASE|flags, nametodraw); + + // draw text cursor for name + if (input->skull < 4) // blink cursor + V_DrawCharacter(skullx, y+3, '_'|flags, false); + + // draw selection + if (input->select != input->cursor) + { + size_t start = min(input->select, input->cursor); + size_t end = max(input->select, input->cursor); + + INT32 startx = 0; + INT32 width = 0; + + // I couldn't figure out one formula so here's bunch of separate cases + if (start < drawstart && end > drawend) // Selection covers whole visible portion of demo name + { + startx = -2; + width = V_StringWidth(nametodraw, V_ALLOWLOWERCASE)+4; + } + else if (start < drawstart) // Only left side of selection is off visible part + { + startx = -2; + size_t len = (end - start) - (drawstart - start); + width = V_SubStringWidth(nametodraw, len, V_ALLOWLOWERCASE)+2; + } + else if (end > drawend) // Only right side of selection is off visible part + { + startx = V_SubStringWidth(nametodraw, start-drawstart, V_ALLOWLOWERCASE); + width = V_StringWidth(nametodraw+(start-drawstart), V_ALLOWLOWERCASE)+2; + } + else // All selection is on visible part + { + startx = V_SubStringWidth(nametodraw, start-drawstart, V_ALLOWLOWERCASE); + width = V_SubStringWidth(nametodraw+(start-drawstart), end-start, V_ALLOWLOWERCASE); + } + + V_DrawFill(x+startx, y, width, 8, 103|V_TRANSLUCENT|flags); + } +} diff --git a/src/m_textinput.h b/src/m_textinput.h index aad5e94ab..1cc1c5005 100644 --- a/src/m_textinput.h +++ b/src/m_textinput.h @@ -2,6 +2,7 @@ #define __M_TEXTINPUT__ #include "doomtype.h" +#include "m_emotes.h" // M_VerifyEmote, M_FindEmote typedef struct textinput_s { size_t cursor; @@ -10,6 +11,8 @@ typedef struct textinput_s { char *buffer; size_t buffer_size; + + unsigned skull; } textinput_t; void M_TextInputInit(textinput_t *input, char *buffer, size_t buffer_size); @@ -20,4 +23,11 @@ void M_TextInputSetString(textinput_t *input, const char *c); 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); + +#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); + #endif diff --git a/src/p_setup.c b/src/p_setup.c index 95205b3e2..cbeff26e1 100644 --- a/src/p_setup.c +++ b/src/p_setup.c @@ -38,6 +38,7 @@ #include "r_fps.h" // R_ResetViewInterpolation in level load #include "s_sound.h" +#include "m_emotes.h" #include "st_stuff.h" #include "w_wad.h" #include "z_zone.h" @@ -9578,6 +9579,10 @@ UINT16 P_PartialAddWadFile(const char *wadfilename, wadcompat_t compat) // R_LoadSpriteInfoLumps(wadnum, numlumps); + // look for emotes + // + M_LoadEmotes(wadnum); + refreshdirmenu &= ~REFRESHDIR_GAMEDATA; // Under usual circumstances we'd wait for REFRESHDIR_ flags to disappear the next frame, but this one's a bit too dangerous for that... partadd_stage = 0; return wadnum;