From 6358d839ee3ed7220a611ce412bcd7127978385b Mon Sep 17 00:00:00 2001 From: James R Date: Fri, 22 Feb 2019 20:18:33 -0800 Subject: [PATCH] Overhaul the map command Added support for map names, matched by substring and keywords too! Added support for two digit MAP codes without the MAP part. Added support for decimal map number. (But who cares.) Gave a better description of command. Supported abbreviated optional parameters. And now REALLY detects incorrect parameters. --- src/d_netcmd.c | 305 ++++++++++++++++++++++++++++++++++++++----------- src/doomdef.h | 1 + src/doomtype.h | 3 + src/m_misc.c | 67 +++++++++++ 4 files changed, 308 insertions(+), 68 deletions(-) diff --git a/src/d_netcmd.c b/src/d_netcmd.c index b14f92b33..68ab3568f 100644 --- a/src/d_netcmd.c +++ b/src/d_netcmd.c @@ -1719,25 +1719,54 @@ void D_MapChange(INT32 mapnum, INT32 newgametype, boolean pultmode, boolean rese } } +/* +Easy macro; declare parm_*id* and define acceptableargc; put in the parameter +to match as a string as *name*. Set *argn* to the number of extra arguments +following the parameter. parm_*id* is filled with the index of the parameter +found and acceptableargc is incremented to match the macro parameters. +Returned is whether the parameter was found. +*/ +#define CHECKPARM( id, name, argn ) \ +( (( parm_ ## id = COM_CheckParm(name) )) &&\ + ( acceptableargc += 1 + argn ) ) +// // 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; + size_t acceptableargc; + size_t parm_force; + size_t parm_gametype; + const char *arg_gametype; + /* debug? */ + size_t parm_noresetplayers; boolean newresetplayers; + + boolean mustmodifygame; + boolean usemapcode = false; + + INT32 newmapnum; + INT32 apromapnum = 0; + + const char *mapname; + size_t mapnamelen; + char *realmapname = NULL; + char *apromapname = NULL; + + /* Keyword matching */ + char *query; + char *key; + UINT8 *freq; + UINT8 freqc; + 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 i; + INT32 d; + char *p; if (client && !IsPlayerAdmin(consoleplayer)) { @@ -1745,62 +1774,217 @@ 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) + acceptableargc = 2;/* map name */ + + (void) + ( + CHECKPARM (force, "-force", 0) || + CHECKPARM (force, "-f", 0) + ); + (void) + ( + CHECKPARM (gametype, "-gametype", 1) || + CHECKPARM (gametype, "-g", 1) || + CHECKPARM (gametype, "-gt", 1) + ); + + (void)CHECKPARM (noresetplayers, "-noresetplayers", 0); + + newresetplayers = !parm_noresetplayers; + + mustmodifygame = + !( netgame || multiplayer ) && + (!modifiedgame || savemoddata ); + + if (mustmodifygame && !parm_force) { - 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 (parm_gametype && !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 (COM_Argc() != acceptableargc) + { + /* 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 = COM_Argv(1); + 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); + 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); + return; + } + usemapcode = true; + } + else + { + query = ZZ_Alloc(strlen(mapname)+1); + freq = ZZ_Calloc(NUMMAPS * sizeof (UINT8)); + + for (i = 0, newmapnum = 1; i < NUMMAPS; ++i, ++newmapnum) + if (mapheaderinfo[i]) + { + realmapname = G_BuildMapTitle(newmapnum); + + /* Now that we found a perfect match no need to fucking guess. */ + if (strnicmp(realmapname, mapname, mapnamelen) == 0) + { + Z_Free(apromapname); + break; + } + + if (apromapnum == 0) + { + /* LEVEL 1--match keywords verbatim */ + if (strcasestr(realmapname, mapname)) + { + apromapnum = newmapnum; + apromapname = realmapname; + realmapname = 0; + } + else/* ...match individual keywords */ + { + strcpy(query, mapname); + for (key = strtok(query, " "); + key; + key = strtok(0, " ")) + { + if (strcasestr(realmapname, key)) + { + freq[i]++; + } + } + } + } + + Z_Free(realmapname);/* leftover old name */ + } + + if (newmapnum == NUMMAPS+1)/* no perfect match--try a substring */ + { + newmapnum = apromapnum; + realmapname = apromapname; + } + + if (newmapnum == 0)/* calculate most queries met! */ + { + freqc = 0; + for (i = 0; i < NUMMAPS; ++i) + { + if (freq[i] > freqc) + { + freqc = freq[i]; + newmapnum = i + 1; + } + } + if (newmapnum) + { + realmapname = G_BuildMapTitle(newmapnum); + } + } + + Z_Free(freq); + Z_Free(query); + } + } + + if (newmapnum == 0 || !mapheaderinfo[newmapnum-1]) + { + CONS_Alert(CONS_ERROR, M_GetText("Could not find any map described as '%s'.\n"), mapname); + return; + } + + if (usemapcode) + { + realmapname = G_BuildMapTitle(newmapnum); + } + + if (mustmodifygame && parm_force) + { + G_SetGameModified(false); + } + + arg_gametype = COM_Argv(parm_gametype + 1); // new gametype value // use current one by default - i = COM_CheckParm("-gametype"); - if (i) + if (parm_gametype) { - if (!multiplayer) - { - CONS_Printf(M_GetText("You can't switch gametypes in single player!\n")); - return; - } - - 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 || parm_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 + 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); + return; + } + else + { + fromlevelselect = + ( netgame || multiplayer ) && + newgametype == gametype && + newgametype == GT_COOP; } } @@ -1811,31 +1995,13 @@ 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); 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) { @@ -1848,7 +2014,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 7acdde966..bed9c09a0 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/m_misc.c b/src/m_misc.c index f7d5cf961..701fba2db 100644 --- a/src/m_misc.c +++ b/src/m_misc.c @@ -1672,6 +1672,73 @@ void strcatbf(char *s1, const char *s2, const char *s3) strcat(s1, tmp); } +/** Locate a substring, case-insensitively. + * Know that I hate this style. -James + * + * \param s The string to search within. + * \param q The substring to find. + * \return a pointer to the located substring, or NULL if it could be found. +*/ +char *strcasestr(const char *s, const char *q) +{ + void **vpp;/* a hack! */ + + size_t qz; + + const char *up; + const char *lp; + + int uc; + int lc; + + qz = strlen(q); + + uc = toupper(*q); + lc = tolower(*q); + + up = s; + lp = s; + + do + { + if (uc > 0) + { + up = strchr(up, uc); + if (!up || ( lc == 0 && lp < up )) + uc = -1; + else + if (strnicmp(q, up, qz) == 0) + uc = 0; + else + up++; + } + if (lc > 0) + { + lp = strchr(lp, lc); + if (!lp || ( uc == 0 && up < lp )) + lc = -1; + else + if (strnicmp(q, lp, qz) == 0) + lc = 0; + else + lp++; + } + } + while (( uc > 0 ) || ( lc > 0 )) ; + + if (uc == 0) + vpp = (void **)&up; + else + vpp = (void **)&lp; + + /* + We can dereference a double void pointer and cast it to remove const. + This works because the original variable (the pointer) is writeable, + but its value is not. + */ + return (char *)*vpp; +} + /** Converts an ASCII Hex string into an integer. Thanks, Borland! * I don't know if this belongs here specifically, but it sure * doesn't belong in p_spec.c, that's for sure