diff --git a/src/d_netcmd.c b/src/d_netcmd.c index 7da0aa605..3fb204aea 100644 --- a/src/d_netcmd.c +++ b/src/d_netcmd.c @@ -1736,25 +1736,151 @@ void D_MapChange(INT32 mapnum, INT32 newgametype, boolean pultmode, boolean rese } } +enum +{ + MAP_COMMAND_FORCE_OPTION, + MAP_COMMAND_GAMETYPE_OPTION, + MAP_COMMAND_NORESETPLAYERS_OPTION, + + NUM_MAP_COMMAND_OPTIONS +}; + +static size_t CheckOptions( + int num_options, + size_t *first_argumentp, + size_t *user_options, + const char ***option_names, + int *option_num_arguments +) +{ + int arguments_used; + size_t first_argument; + + int i; + const char **pp; + const char *name; + size_t n; + + arguments_used = 0; + first_argument = COM_Argc(); + + for (i = 0; i < num_options; ++i) + { + pp = option_names[i]; + name = *pp; + do + { + if (( n = COM_CheckParm(name) )) + { + user_options[i] = n; + arguments_used += 1 + option_num_arguments[i]; + if (n < first_argument) + first_argument = n; + } + } + while (( name = *++pp )) ; + } + + (*first_argumentp) = first_argument; + + return arguments_used; +} + +static char * +ConcatCommandArgv (int start, int end) +{ + char *final; + + size_t size; + + int i; + char *p; + + size = 0; + + for (i = start; i < end; ++i) + { + /* + one space after each argument, but terminating + character on final argument + */ + size += strlen(COM_Argv(i)) + 1; + } + + final = ZZ_Alloc(size); + p = final; + + --end;/* handle the final argument separately */ + for (i = start; i < end; ++i) + { + p += sprintf(p, "%s ", COM_Argv(i)); + } + /* at this point "end" is actually the last argument's position */ + strcpy(p, COM_Argv(end)); + + return final; +} + +// // Warp to map code. // Called either from map console command, or idclev cheat. // +// Largely rewritten by James. +// static void Command_Map_f(void) { - const char *mapname; - size_t i; - INT32 newmapnum; + const char *force_option_names[] = + { + "-force", + "-f", + NULL + }; + const char *gametype_option_names[] = + { + "-gametype", + "-g", + "-gt", + NULL + }; + const char *noresetplayers_option_names[] = + { + "-noresetplayers", + NULL + }; + const char **option_names[] = + { + force_option_names, + gametype_option_names, + noresetplayers_option_names, + }; + int option_num_arguments[] = + { + 0,/* -force */ + 1,/* -gametype */ + 0,/* -noresetplayers */ + }; + + size_t acceptableargc;/* (this includes the command name itself!) */ + size_t first_argument; + + size_t user_options [NUM_MAP_COMMAND_OPTIONS] = {0}; + + const char *arg_gametype; boolean newresetplayers; + + boolean mustmodifygame; + boolean usemapcode = false; + + INT32 newmapnum; + + char * mapname; + size_t mapnamelen; + char *realmapname = NULL; + INT32 newgametype = gametype; - // max length of command: map map03 -gametype coop -noresetplayers -force - // 1 2 3 4 5 6 - // = 8 arg max - if (COM_Argc() < 2 || COM_Argc() > 8) - { - CONS_Printf(M_GetText("map [-gametype [-force]: warp to map\n")); - return; - } + INT32 d; + char *p; if (client && !IsPlayerAdmin(consoleplayer)) { @@ -1762,62 +1888,145 @@ static void Command_Map_f(void) return; } - // internal wad lump always: map command doesn't support external files as in doom legacy - if (W_CheckNumForName(COM_Argv(1)) == LUMPERROR) + /* map name + options */ + acceptableargc = 2 + CheckOptions(NUM_MAP_COMMAND_OPTIONS, + &first_argument, + user_options, option_names, option_num_arguments); + + newresetplayers = !user_options[MAP_COMMAND_NORESETPLAYERS_OPTION]; + + mustmodifygame = + !( netgame || multiplayer ) && + (!modifiedgame || savemoddata ); + + if (mustmodifygame && !user_options[MAP_COMMAND_FORCE_OPTION]) { - CONS_Alert(CONS_ERROR, M_GetText("Internal game level '%s' not found\n"), COM_Argv(1)); + /* May want to be more descriptive? */ + CONS_Printf(M_GetText("Sorry, level change disabled in single player.\n")); return; } - if (!(netgame || multiplayer) && (!modifiedgame || savemoddata)) - { - if (COM_CheckParm("-force")) - G_SetGameModified(false); - else - { - CONS_Printf(M_GetText("Sorry, level change disabled in single player.\n")); - return; - } - } - - newresetplayers = !COM_CheckParm("-noresetplayers"); - if (!newresetplayers && !cv_debug) { CONS_Printf(M_GetText("DEVMODE must be enabled.\n")); return; } - mapname = COM_Argv(1); - if (strlen(mapname) != 5 - || (newmapnum = M_MapNumber(mapname[3], mapname[4])) == 0) + if (user_options[MAP_COMMAND_GAMETYPE_OPTION] && !multiplayer) { - CONS_Alert(CONS_ERROR, M_GetText("Invalid level name %s\n"), mapname); + CONS_Printf(M_GetText("You can't switch gametypes in single player!\n")); return; } - // Ultimate Mode only in SP via menu - if (netgame || multiplayer) - ultimatemode = false; + /* If the first argument is an option, you fucked up. */ + if (COM_Argc() < acceptableargc || first_argument == 1) + { + /* I'm going over the fucking lines and I DON'T CAREEEEE */ + CONS_Printf("map [-gametype ] [-force]:\n"); + CONS_Printf(M_GetText( + "Warp to a map, by its name, two character code, with optional \"MAP\" prefix, or by its number (though why would you).\n" + "All parameters are case-insensitive.\n" + "* \"-force\" may be shortened to \"-f\".\n" + "* \"-gametype\" may be shortened to \"-g\" or \"-gt\".\n")); + return; + } + + mapname = ConcatCommandArgv(1, first_argument); + mapnamelen = strlen(mapname); + + if (mapnamelen == 2)/* maybe two digit code */ + { + if (( newmapnum = M_MapNumber(mapname[0], mapname[1]) )) + usemapcode = true; + } + else if (mapnamelen == 5 && strnicmp(mapname, "MAP", 3) == 0) + { + if (( newmapnum = M_MapNumber(mapname[3], mapname[4]) ) == 0) + { + CONS_Alert(CONS_ERROR, M_GetText("Invalid map code '%s'.\n"), mapname); + Z_Free(mapname); + return; + } + usemapcode = true; + } + + if (!usemapcode) + { + /* Now detect map number in base 10, which no one asked for. */ + newmapnum = strtol(mapname, &p, 10); + if (*p == '\0')/* we got it */ + { + if (newmapnum < 1 || newmapnum > NUMMAPS) + { + CONS_Alert(CONS_ERROR, M_GetText("Invalid map number %d.\n"), newmapnum); + Z_Free(mapname); + return; + } + usemapcode = true; + } + else + { + newmapnum = G_FindMap(mapname, &realmapname, NULL, NULL); + } + } + + if (newmapnum == 0 || !mapheaderinfo[newmapnum-1]) + { + CONS_Alert(CONS_ERROR, M_GetText("Could not find any map described as '%s'.\n"), mapname); + Z_Free(mapname); + return; + } + + if (usemapcode) + { + realmapname = G_BuildMapTitle(newmapnum); + } + + if (mustmodifygame && user_options[MAP_COMMAND_FORCE_OPTION]) + { + G_SetGameModified(false); + } // new gametype value // use current one by default - i = COM_CheckParm("-gametype"); - if (i) + if (user_options[MAP_COMMAND_GAMETYPE_OPTION]) { - if (!multiplayer) - { - CONS_Printf(M_GetText("You can't switch gametypes in single player!\n")); - return; - } + arg_gametype = COM_Argv(user_options[MAP_COMMAND_GAMETYPE_OPTION] + 1); - newgametype = G_GetGametypeByName(COM_Argv(i+1)); + newgametype = G_GetGametypeByName(arg_gametype); if (newgametype == -1) // reached end of the list with no match { - INT32 j = atoi(COM_Argv(i+1)); // assume they gave us a gametype number, which is okay too - if (j >= 0 && j < NUMGAMETYPES) - newgametype = (INT16)j; + d = atoi(arg_gametype); + // assume they gave us a gametype number, which is okay too + if (d >= 0 && d < NUMGAMETYPES) + newgametype = d; + } + } + + // don't use a gametype the map doesn't support + if (cv_debug || user_options[MAP_COMMAND_FORCE_OPTION] || cv_skipmapcheck.value) + fromlevelselect = false; // The player wants us to trek on anyway. Do so. + // G_TOLFlag handles both multiplayer gametype and ignores it for !multiplayer + else + { + if (!( + mapheaderinfo[newmapnum-1] && + mapheaderinfo[newmapnum-1]->typeoflevel & G_TOLFlag(newgametype) + )) + { + CONS_Alert(CONS_WARNING, M_GetText("%s (%s) doesn't support %s mode!\n(Use -force to override)\n"), realmapname, G_BuildMapName(newmapnum), + (multiplayer ? gametype_cons_t[newgametype].strvalue : "Single Player")); + Z_Free(realmapname); + Z_Free(mapname); + return; + } + else + { + fromlevelselect = + ( netgame || multiplayer ) && + newgametype == gametype && + newgametype == GT_COOP; } } @@ -1828,31 +2037,14 @@ static void Command_Map_f(void) if (!dedicated && M_MapLocked(newmapnum)) { CONS_Alert(CONS_NOTICE, M_GetText("You need to unlock this level before you can warp to it!\n")); + Z_Free(realmapname); + Z_Free(mapname); return; } - // don't use a gametype the map doesn't support - if (cv_debug || COM_CheckParm("-force") || cv_skipmapcheck.value) - fromlevelselect = false; // The player wants us to trek on anyway. Do so. - // G_TOLFlag handles both multiplayer gametype and ignores it for !multiplayer - // Alternatively, bail if the map header is completely missing anyway. - else if (!mapheaderinfo[newmapnum-1] - || !(mapheaderinfo[newmapnum-1]->typeoflevel & G_TOLFlag(newgametype))) - { - char gametypestring[32] = "Single Player"; - - if (multiplayer) - { - if (newgametype >= 0 && newgametype < NUMGAMETYPES - && Gametype_Names[newgametype]) - strcpy(gametypestring, Gametype_Names[newgametype]); - } - - CONS_Alert(CONS_WARNING, M_GetText("%s doesn't support %s mode!\n(Use -force to override)\n"), mapname, gametypestring); - return; - } - else - fromlevelselect = ((netgame || multiplayer) && ((gametype == newgametype) && (newgametype == GT_COOP))); + // Ultimate Mode only in SP via menu + if (netgame || multiplayer) + ultimatemode = false; if (tutorialmode && tutorialgcs) { @@ -1865,7 +2057,10 @@ static void Command_Map_f(void) tutorialmode = false; // warping takes us out of tutorial mode D_MapChange(newmapnum, newgametype, false, newresetplayers, 0, false, fromlevelselect); + + Z_Free(realmapname); } +#undef CHECKPARM /** Receives a map command and changes the map. * diff --git a/src/doomdef.h b/src/doomdef.h index 676c86e0d..b58d59259 100644 --- a/src/doomdef.h +++ b/src/doomdef.h @@ -495,6 +495,7 @@ extern boolean capslock; // if we ever make our alloc stuff... #define ZZ_Alloc(x) Z_Malloc(x, PU_STATIC, NULL) +#define ZZ_Calloc(x) Z_Calloc(x, PU_STATIC, NULL) // i_system.c, replace getchar() once the keyboard has been appropriated INT32 I_GetKey(void); diff --git a/src/doomtype.h b/src/doomtype.h index 5f60e21b5..746463cd2 100644 --- a/src/doomtype.h +++ b/src/doomtype.h @@ -115,6 +115,9 @@ typedef long ssize_t; #define strnicmp(x,y,n) strncasecmp(x,y,n) #endif +char *strcasestr(const char *in, const char *what); +#define stristr strcasestr + #if defined (macintosh) //|| defined (__APPLE__) //skip all boolean/Boolean crap #define true 1 #define false 0 diff --git a/src/g_game.c b/src/g_game.c index 18d0cdfe8..9677e81c9 100644 --- a/src/g_game.c +++ b/src/g_game.c @@ -4047,6 +4047,187 @@ char *G_BuildMapTitle(INT32 mapnum) return title; } +static void measurekeywords(mapsearchfreq_t *fr, + struct searchdim **dimp, UINT8 *cuntp, + const char *s, const char *q, boolean wanttable) +{ + char *qp; + char *sp; + if (wanttable) + (*dimp) = Z_Realloc((*dimp), 255 * sizeof (struct searchdim), + PU_STATIC, NULL); + for (qp = strtok(va("%s", q), " "); + qp && fr->total < 255; + qp = strtok(0, " ")) + { + if (( sp = strcasestr(s, qp) )) + { + if (wanttable) + { + (*dimp)[(*cuntp)].pos = sp - s; + (*dimp)[(*cuntp)].siz = strlen(qp); + } + (*cuntp)++; + fr->total++; + } + } + if (wanttable) + (*dimp) = Z_Realloc((*dimp), (*cuntp) * sizeof (struct searchdim), + PU_STATIC, NULL); +} + +static void writesimplefreq(mapsearchfreq_t *fr, INT32 *frc, + INT32 mapnum, UINT8 pos, UINT8 siz) +{ + fr[(*frc)].mapnum = mapnum; + fr[(*frc)].matchd = ZZ_Alloc(sizeof (struct searchdim)); + fr[(*frc)].matchd[0].pos = pos; + fr[(*frc)].matchd[0].siz = siz; + fr[(*frc)].matchc = 1; + fr[(*frc)].total = 1; + (*frc)++; +} + +INT32 G_FindMap(const char *mapname, char **foundmapnamep, + mapsearchfreq_t **freqp, INT32 *freqcp) +{ + INT32 newmapnum = 0; + INT32 mapnum; + INT32 apromapnum = 0; + + size_t mapnamelen; + char *realmapname = NULL; + char *newmapname = NULL; + char *apromapname = NULL; + char *aprop = NULL; + + mapsearchfreq_t *freq; + boolean wanttable; + INT32 freqc; + UINT8 frequ; + + INT32 i; + + mapnamelen = strlen(mapname); + + /* Count available maps; how ugly. */ + for (i = 0, freqc = 0; i < NUMMAPS; ++i) + { + if (mapheaderinfo[i]) + freqc++; + } + + freq = ZZ_Calloc(freqc * sizeof (mapsearchfreq_t)); + + wanttable = !!( freqp ); + + freqc = 0; + for (i = 0, mapnum = 1; i < NUMMAPS; ++i, ++mapnum) + if (mapheaderinfo[i]) + { + if (!( realmapname = G_BuildMapTitle(mapnum) )) + continue; + + aprop = realmapname; + + /* Now that we found a perfect match no need to fucking guess. */ + if (strnicmp(realmapname, mapname, mapnamelen) == 0) + { + if (wanttable) + { + writesimplefreq(freq, &freqc, mapnum, 0, mapnamelen); + } + if (newmapnum == 0) + { + newmapnum = mapnum; + newmapname = realmapname; + realmapname = 0; + Z_Free(apromapname); + if (!wanttable) + break; + } + } + else + if (apromapnum == 0 || wanttable) + { + /* LEVEL 1--match keywords verbatim */ + if (( aprop = strcasestr(realmapname, mapname) )) + { + if (wanttable) + { + writesimplefreq(freq, &freqc, + mapnum, aprop - realmapname, mapnamelen); + } + if (apromapnum == 0) + { + apromapnum = mapnum; + apromapname = realmapname; + realmapname = 0; + } + } + else/* ...match individual keywords */ + { + freq[freqc].mapnum = mapnum; + measurekeywords(&freq[freqc], + &freq[freqc].matchd, &freq[freqc].matchc, + realmapname, mapname, wanttable); + if (freq[freqc].total) + freqc++; + } + } + + Z_Free(realmapname);/* leftover old name */ + } + + if (newmapnum == 0)/* no perfect match--try a substring */ + { + newmapnum = apromapnum; + newmapname = apromapname; + } + + if (newmapnum == 0)/* calculate most queries met! */ + { + frequ = 0; + for (i = 0; i < freqc; ++i) + { + if (freq[i].total > frequ) + { + frequ = freq[i].total; + newmapnum = freq[i].mapnum; + } + } + if (newmapnum) + { + newmapname = G_BuildMapTitle(newmapnum); + } + } + + if (freqp) + (*freqp) = freq; + else + Z_Free(freq); + + if (freqcp) + (*freqcp) = freqc; + + if (foundmapnamep) + (*foundmapnamep) = newmapname; + else + Z_Free(newmapname); + + return newmapnum; +} + +void G_FreeMapSearch(mapsearchfreq_t *freq, INT32 freqc) +{ + INT32 i; + for (i = 0; i < freqc; ++i) + { + Z_Free(freq[i].matchd); + } + Z_Free(freq); +} + // // DEMO RECORDING // diff --git a/src/g_game.h b/src/g_game.h index 22abae17d..2489ff097 100644 --- a/src/g_game.h +++ b/src/g_game.h @@ -108,6 +108,27 @@ void G_InitNew(UINT8 pultmode, const char *mapname, boolean resetplayer, boolean skipprecutscene, boolean FLS); char *G_BuildMapTitle(INT32 mapnum); +struct searchdim +{ + UINT8 pos; + UINT8 siz; +}; + +typedef struct +{ + INT16 mapnum; + UINT8 matchc; + struct searchdim *matchd;/* offset that a pattern was matched */ + UINT8 keywhc; + struct searchdim *keywhd;/* ...in KEYWORD */ + UINT8 total;/* total hits */ +} +mapsearchfreq_t; + +INT32 G_FindMap(const char *query, char **foundmapnamep, + mapsearchfreq_t **freqp, INT32 *freqc); +void G_FreeMapSearch(mapsearchfreq_t *freq, INT32 freqc); + // XMOD spawning mapthing_t *G_FindCTFStart(INT32 playernum); mapthing_t *G_FindMatchStart(INT32 playernum); diff --git a/src/strcasestr.c b/src/strcasestr.c new file mode 100644 index 000000000..2077dc3ff --- /dev/null +++ b/src/strcasestr.c @@ -0,0 +1,110 @@ +/* +strcasestr -- case insensitive substring searching function. +*/ +/* +Copyright 2019 James R. +All rights reserved. + +Redistribution and use in source forms, with or without modification, is +permitted provided that the following condition is met: + +1. Redistributions of source code must retain the above copyright notice, this + condition and the following disclaimer. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#define SWAP( a, b ) \ +(\ + (a) ^= (b),\ + (b) ^= (a),\ + (a) ^= (b)\ +) + +static inline int +trycmp (char **pp, char *cp, + const char *q, size_t qn) +{ + char *p; + p = (*pp); + if (strncasecmp(p, q, qn) == 0) + return 0; + (*pp) = strchr(&p[1], (*cp)); + return 1; +} + +static inline void +swapp (char ***ppap, char ***ppbp, char **cpap, char **cpbp) +{ + SWAP(*(intptr_t *)ppap, *(intptr_t *)ppbp); + SWAP(*(intptr_t *)cpap, *(intptr_t *)cpbp); +} + +char * +strcasestr (const char *s, const char *q) +{ + size_t qn; + + char uc; + char lc; + + char *up; + char *lp; + + char **ppa; + char **ppb; + + char *cpa; + char *cpb; + + uc = toupper(*q); + lc = tolower(*q); + + up = strchr(s, uc); + lp = strchr(s, lc); + + if (!( (intptr_t)up|(intptr_t)lp )) + return 0; + + if (!lp || up < lp) + { + ppa = &up; + ppb = &lp; + + cpa = &uc; + cpb = &lc; + } + else + { + ppa = &lp; + ppb = &up; + + cpa = &lc; + cpb = &uc; + } + + qn = strlen(q); + + for (;;) + { + if (trycmp(ppa, cpa, q, qn) == 0) + return (*ppa); + + if (!( (intptr_t)up|(intptr_t)lp )) + break; + + if (!(*ppa) || ( (*ppb) && (*ppb) < (*ppa) )) + swapp(&ppa, &ppb, &cpa, &cpb); + } + + return 0; +} diff --git a/src/string.c b/src/string.c index 2a03e8729..c415e5245 100644 --- a/src/string.c +++ b/src/string.c @@ -2,6 +2,7 @@ //----------------------------------------------------------------------------- // Copyright (C) 2006 by Graue. // Copyright (C) 2006-2018 by Sonic Team Junior. +// Copyright (C) 2019 by James R. // // This program is free software distributed under the // terms of the GNU General Public License, version 2. @@ -50,3 +51,5 @@ size_t strlcpy(char *dst, const char *src, size_t siz) } #endif + +#include "strcasestr.c"