diff --git a/core/HalfLife2.cpp b/core/HalfLife2.cpp index 0e129448..0f4695b3 100644 --- a/core/HalfLife2.cpp +++ b/core/HalfLife2.cpp @@ -1208,53 +1208,72 @@ const char *CHalfLife2::GetEntityClassname(CBaseEntity *pEntity) return *(const char **)(((unsigned char *)pEntity) + offset); } -#if SOURCE_ENGINE >= SE_LEFT4DEAD -static bool ResolveFuzzyMapName(const char *fuzzyName, char *outFullname, int size) +SMFindMapResult CHalfLife2::FindMap(char *pMapName, int nMapNameMax) { +#if SOURCE_ENGINE >= SE_LEFT4DEAD + static char mapNameTmp[PLATFORM_MAX_PATH]; + g_SourceMod.Format(mapNameTmp, sizeof(mapNameTmp), "maps%c%s.bsp", PLATFORM_SEP_CHAR, pMapName); + if (filesystem->FileExists(mapNameTmp, "GAME")) + { + // If this is already an exact match, don't attempt to autocomplete it further (de_dust -> de_dust2). + // ... but still check that map file is actually valid. + // We check FileExists first to avoid console message about IsMapValid with invalid map. + return engine->IsMapValid(pMapName) == 0 ? SMFindMapResult::NotFound : SMFindMapResult::Found; + } + static ConCommand *pHelperCmd = g_pCVar->FindCommand("changelevel"); + + // This shouldn't happen. if (!pHelperCmd || !pHelperCmd->CanAutoComplete()) - return false; + { + return engine->IsMapValid(pMapName) == 0 ? SMFindMapResult::NotFound : SMFindMapResult::Found; + } static size_t helperCmdLen = strlen(pHelperCmd->GetName()); CUtlVector results; - pHelperCmd->AutoCompleteSuggest(fuzzyName, results); + pHelperCmd->AutoCompleteSuggest(pMapName, results); if (results.Count() == 0) - return false; + return SMFindMapResult::NotFound; // Results come back as you'd see in autocomplete. (ie. "changelevel fullmapnamehere"), // so skip ahead to start of map path/name // Like the engine, we're only going to deal with the first match. - strncopy(outFullname, &results[0][helperCmdLen + 1], size); - - return true; -} + bool bExactMatch = Q_strcmp(pMapName, &results[0][helperCmdLen + 1]) == 0; + if (bExactMatch) + { + return SMFindMapResult::Found; + } + else + { + strncopy(pMapName, &results[0][helperCmdLen + 1], nMapNameMax); + return SMFindMapResult::FuzzyMatch; + } +#elif SOURCE_ENGINE == SE_TF2 + // Save off name passed in so that we can compare to output. + // There is a bug where eFindMap_FuzzyMap is never returned, even for fuzzy matches. + char *pOriginal = sm_strdup(pMapName); + SMFindMapResult res = static_cast(engine->FindMap(pMapName, nMapNameMax)); + bool bExactMatch = strcmp(pOriginal, pMapName) == 0; + delete [] pOriginal; + if (res == SMFindMapResult::Found && !bExactMatch) + return SMFindMapResult::FuzzyMatch; + else + return res; +#else + return engine->IsMapValid(pMapName) == 0 ? SMFindMapResult::NotFound : SMFindMapResult::Found; #endif +} bool CHalfLife2::IsMapValid(const char *map) { if (!map || !map[0]) return false; - - bool ret; -#if SOURCE_ENGINE == SE_TF2 - char szTmp[PLATFORM_MAX_PATH]; + + static char szTmp[PLATFORM_MAX_PATH]; strncopy(szTmp, map, sizeof(szTmp)); - ret = engine->FindMap(szTmp, sizeof(szTmp)) != eFindMap_NotFound; -#else - ret = engine->IsMapValid(map); -#if SOURCE_ENGINE >= SE_LEFT4DEAD - if (!ret) - { - static char szFuzzyName[PLATFORM_MAX_PATH]; - if (ResolveFuzzyMapName(map, szFuzzyName, sizeof(szFuzzyName))) - { - ret = engine->IsMapValid(szFuzzyName); - } - } -#endif -#endif // SE_TF2 - return ret; + + return FindMap(szTmp, sizeof(szTmp)) != SMFindMapResult::NotFound; } diff --git a/core/HalfLife2.h b/core/HalfLife2.h index 2529f76b..f48f687d 100644 --- a/core/HalfLife2.h +++ b/core/HalfLife2.h @@ -129,6 +129,16 @@ public: #endif }; +// Corresponds to TF2's eFindMapResult in eiface.h +// Not yet in other games, but eventually in others on same branch. +enum class SMFindMapResult : cell_t { + Found, + NotFound, + FuzzyMatch, + NonCanonical, + PossiblyAvailable +}; + class CHalfLife2 : public SMGlobalClass, public IGameHelpers @@ -174,6 +184,7 @@ public: //IGameHelpers const char *GetEntityClassname(edict_t *pEdict); const char *GetEntityClassname(CBaseEntity *pEntity); bool IsMapValid(const char *map); + SMFindMapResult FindMap(char *pMapName, int nMapNameMax); public: void AddToFakeCliCmdQueue(int client, int userid, const char *cmd); void ProcessFakeCliCmdQueue(); diff --git a/core/smn_halflife.cpp b/core/smn_halflife.cpp index 41667aff..b32cb9e2 100644 --- a/core/smn_halflife.cpp +++ b/core/smn_halflife.cpp @@ -67,6 +67,16 @@ static cell_t IsMapValid(IPluginContext *pContext, const cell_t *params) return g_HL2.IsMapValid(map); } +static cell_t FindMap(IPluginContext *pContext, const cell_t *params) +{ + char *pMapname; + pContext->LocalToString(params[1], &pMapname); + + cell_t size = params[2]; + + return static_cast(g_HL2.FindMap(pMapname, size)); +} + static cell_t IsDedicatedServer(IPluginContext *pContext, const cell_t *params) { return engine->IsDedicatedServer(); @@ -626,6 +636,7 @@ REGISTER_NATIVES(halflifeNatives) {"GetRandomInt", GetRandomInt}, {"IsDedicatedServer", IsDedicatedServer}, {"IsMapValid", IsMapValid}, + {"FindMap", FindMap}, {"SetFakeClientConVar", SetFakeClientConVar}, {"SetRandomSeed", SetRandomSeed}, {"PrecacheModel", PrecacheModel}, diff --git a/plugins/include/halflife.inc b/plugins/include/halflife.inc index 1fe6edcd..b50a4f84 100644 --- a/plugins/include/halflife.inc +++ b/plugins/include/halflife.inc @@ -91,6 +91,25 @@ enum EngineVersion Engine_BlackMesa, /**< Black Mesa Multiplayer */ }; +enum FindMapResult +{ + // A direct match for this name was found + FindMap_Found, + // No match for this map name could be found. + FindMap_NotFound, + // A fuzzy match for this map name was found. + // Ex: cp_dust -> cp_dustbowl, c1m1 -> c1m1_hotel + // Only supported for maps that the engine knows about. (This excludes workshop maps on Orangebox). + FindMap_FuzzyMatch, + // A non-canonical match for this map name was found. + // Ex: workshop/1234 -> workshop/cp_qualified_name.ugc1234 + // Only supported on "Orangebox" games with workshop support. + FindMap_NonCanonical, + // No currently available match for this map name could be found, but it may be possible to load + // Only supported on "Orangebox" games with workshop support. + FindMap_PossiblyAvailable +}; + #define INVALID_ENT_REFERENCE 0xFFFFFFFF /** @@ -136,6 +155,17 @@ native GetRandomInt(nmin, nmax); */ native bool:IsMapValid(const String:map[]); +/** + * Returns whether a full or partial map name is found or can be resolved + * + * @param map Map name (usually same as map path relative to maps/ dir, + * excluding .bsp extension). If result is FindMap_FuzzyMatch + * or FindMap_NonCanonical, this will be updated to the full path. + * @param maxlen Maximum length to write to map var. + * @return Result of the find operation. Not all result types are supported on all games. + */ +native FindMapResult FindMap(char[] map, int maxlen); + /** * Returns whether the server is dedicated. * diff --git a/plugins/testsuite/findmap.sp b/plugins/testsuite/findmap.sp new file mode 100644 index 00000000..534e2b54 --- /dev/null +++ b/plugins/testsuite/findmap.sp @@ -0,0 +1,31 @@ +#include + +public void OnPluginStart() +{ + RegServerCmd("test_findmap", test_findmap); +} + +public Action test_findmap( int argc ) +{ + char mapName[PLATFORM_MAX_PATH]; + GetCmdArg(1, mapName, sizeof(mapName)); + + char resultName[16]; + switch (FindMap(mapName, sizeof(mapName))) + { + case FindMap_Found: + strcopy(resultName, sizeof(resultName), "Found"); + case FindMap_NotFound: + strcopy(resultName, sizeof(resultName), "NotFound"); + case FindMap_FuzzyMatch: + strcopy(resultName, sizeof(resultName), "FuzzyMatch"); + case FindMap_NonCanonical: + strcopy(resultName, sizeof(resultName), "NonCanonical"); + case FindMap_PossiblyAvailable: + strcopy(resultName, sizeof(resultName), "PossiblyAvailable"); + } + + PrintToServer("FindMap says %s - \"%s\"", resultName, mapName); + + return Plugin_Handled; +} \ No newline at end of file