From a8ecd7fea49257947acc9c2d7f1e16574305af49 Mon Sep 17 00:00:00 2001 From: David Anderson Date: Sun, 2 Dec 2007 02:10:37 +0000 Subject: [PATCH] - added beefy new ReadMapList() native - admin menu now uses ReadMapList() - added UTIL_TrimWhitespace() to stringutils - moved GetFileTime() implementation to ILibrarySys - cleaned up sorting include a bit - removed adminmenu_maplist.ini, since it's specified by maplists.cfg - added maplists.cfg --HG-- extra : convert_revision : svn%3A39bc706e-5318-0410-9160-8a85361fbb7c/trunk%401744 --- configs/adminmenu_maplist.ini | 5 - configs/maplists.cfg | 34 ++ core/CrazyDebugger.cpp | 2 +- core/Makefile.ep1 | 2 +- core/Makefile.ep2 | 2 +- core/Makefile.orig | 2 +- core/msvc8/sourcemod_mm.vcproj | 4 + core/sm_stringutil.cpp | 31 ++ core/sm_stringutil.h | 1 + core/smn_filesystem.cpp | 27 +- core/smn_maplists.cpp | 659 +++++++++++++++++++++++++++++++++ core/systems/LibrarySys.cpp | 30 ++ core/systems/LibrarySys.h | 1 + plugins/basecommands.sp | 5 +- plugins/basecommands/map.sp | 114 ++---- plugins/include/sorting.inc | 6 +- plugins/include/sourcemod.inc | 64 ++++ public/ILibrarySys.h | 22 +- 18 files changed, 880 insertions(+), 131 deletions(-) delete mode 100644 configs/adminmenu_maplist.ini create mode 100644 configs/maplists.cfg create mode 100644 core/smn_maplists.cpp diff --git a/configs/adminmenu_maplist.ini b/configs/adminmenu_maplist.ini deleted file mode 100644 index ae9bdf57..00000000 --- a/configs/adminmenu_maplist.ini +++ /dev/null @@ -1,5 +0,0 @@ -// adminmenu_maplist.ini -// -// List maps here to be added to the map and votemap sections of the admin menu -// - diff --git a/configs/maplists.cfg b/configs/maplists.cfg new file mode 100644 index 00000000..032b4a7e --- /dev/null +++ b/configs/maplists.cfg @@ -0,0 +1,34 @@ +/** + * Use this file to configure map lists. + * + * Each section is a map list that plugins can use. For example, the Admin Menu + * requests an "admin menu" map list, and you can control which maps appear via + * this file. + * + * Each section must have a property that explains where to read the maps from. + * There are two properties: + * + * target - Redirect the request to another section. + * file - Read a file of map names, in mapcycle.txt format. + * + * There is one section by default, called "mapcyclefile" - it is mapped to the + * mapcycle.txt file, or whatever the contents of your mapcyclefile cvar is. + * + * If a plugin requests a map list file which doesn't exist, or is empty, SourceMod + * tries the "default" section, and then the "mapcyclefile" section. + */ +"MapLists" +{ + /** + * Default requests go right to the mapcyclefile. + */ + "default" + { + "target" "mapcyclefile" + } + + "admin menu" + { + "file" "addons/sourcemod/configs/adminmenu_maplist.ini" + } +} diff --git a/core/CrazyDebugger.cpp b/core/CrazyDebugger.cpp index 7960191d..492766ce 100644 --- a/core/CrazyDebugger.cpp +++ b/core/CrazyDebugger.cpp @@ -29,7 +29,7 @@ * Version: $Id$ */ -#if defined DEBUG +#if 0 #include "sm_globals.h" #include "sourcemm_api.h" #include "Tlhelp32.h" diff --git a/core/Makefile.ep1 b/core/Makefile.ep1 index 46064b72..c25cac9c 100644 --- a/core/Makefile.ep1 +++ b/core/Makefile.ep1 @@ -28,7 +28,7 @@ OBJECTS = AdminCache.cpp CDataPack.cpp ConCmdManager.cpp ConVarManager.cpp CoreC OBJECTS += smn_admin.cpp smn_bitbuffer.cpp smn_console.cpp smn_core.cpp \ smn_datapacks.cpp smn_entities.cpp smn_events.cpp smn_fakenatives.cpp \ smn_filesystem.cpp smn_float.cpp smn_functions.cpp smn_gameconfigs.cpp smn_halflife.cpp \ - smn_handles.cpp smn_keyvalues.cpp smn_banning.cpp \ + smn_handles.cpp smn_keyvalues.cpp smn_banning.cpp smn_maplist.cpp \ smn_lang.cpp smn_player.cpp smn_string.cpp smn_sorting.cpp smn_textparse.cpp smn_timers.cpp \ smn_usermsgs.cpp smn_menus.cpp smn_database.cpp smn_vector.cpp smn_adt_array.cpp OBJECTS += systems/ExtensionSys.cpp systems/ForwardSys.cpp systems/HandleSys.cpp \ diff --git a/core/Makefile.ep2 b/core/Makefile.ep2 index 4ee53df8..1d3ff939 100644 --- a/core/Makefile.ep2 +++ b/core/Makefile.ep2 @@ -28,7 +28,7 @@ OBJECTS = AdminCache.cpp CDataPack.cpp ConCmdManager.cpp ConVarManager.cpp CoreC OBJECTS += smn_admin.cpp smn_bitbuffer.cpp smn_console.cpp smn_core.cpp \ smn_datapacks.cpp smn_entities.cpp smn_events.cpp smn_fakenatives.cpp \ smn_filesystem.cpp smn_float.cpp smn_functions.cpp smn_gameconfigs.cpp smn_halflife.cpp \ - smn_handles.cpp smn_keyvalues.cpp smn_banning.cpp \ + smn_handles.cpp smn_keyvalues.cpp smn_banning.cpp smn_maplist.cpp \ smn_lang.cpp smn_player.cpp smn_string.cpp smn_sorting.cpp smn_textparse.cpp smn_timers.cpp \ smn_usermsgs.cpp smn_menus.cpp smn_database.cpp smn_vector.cpp smn_adt_array.cpp OBJECTS += systems/ExtensionSys.cpp systems/ForwardSys.cpp systems/HandleSys.cpp \ diff --git a/core/Makefile.orig b/core/Makefile.orig index 4b716371..eec753e1 100644 --- a/core/Makefile.orig +++ b/core/Makefile.orig @@ -28,7 +28,7 @@ OBJECTS = AdminCache.cpp CDataPack.cpp ConCmdManager.cpp ConVarManager.cpp CoreC OBJECTS += smn_admin.cpp smn_bitbuffer.cpp smn_console.cpp smn_core.cpp \ smn_datapacks.cpp smn_entities.cpp smn_events.cpp smn_fakenatives.cpp \ smn_filesystem.cpp smn_float.cpp smn_functions.cpp smn_gameconfigs.cpp smn_halflife.cpp \ - smn_handles.cpp smn_keyvalues.cpp smn_banning.cpp \ + smn_handles.cpp smn_keyvalues.cpp smn_banning.cpp smn_maplist.cpp \ smn_lang.cpp smn_player.cpp smn_string.cpp smn_sorting.cpp smn_textparse.cpp smn_timers.cpp \ smn_usermsgs.cpp smn_menus.cpp smn_database.cpp smn_vector.cpp smn_adt_array.cpp OBJECTS += systems/ExtensionSys.cpp systems/ForwardSys.cpp systems/HandleSys.cpp \ diff --git a/core/msvc8/sourcemod_mm.vcproj b/core/msvc8/sourcemod_mm.vcproj index 2d01ff19..823a0756 100644 --- a/core/msvc8/sourcemod_mm.vcproj +++ b/core/msvc8/sourcemod_mm.vcproj @@ -1378,6 +1378,10 @@ RelativePath="..\smn_lang.cpp" > + + diff --git a/core/sm_stringutil.cpp b/core/sm_stringutil.cpp index e94ff44e..70586d96 100644 --- a/core/sm_stringutil.cpp +++ b/core/sm_stringutil.cpp @@ -1275,3 +1275,34 @@ char *UTIL_ReplaceEx(char *subject, size_t maxLen, const char *search, size_t se return NULL; } + +char *UTIL_TrimWhitespace(char *str, size_t &len) +{ + char *end = str + len - 1; + + if (!len) + { + return str; + } + + /* Iterate backwards through string until we reach first non-whitespace char */ + while (end >= str && textparsers->IsWhitespace(end)) + { + end--; + len--; + } + + /* Replace first whitespace char (at the end) with null terminator. + * If there is none, we're just replacing the null terminator. + */ + *(end + 1) = '\0'; + + while (*str != '\0' && textparsers->IsWhitespace(str)) + { + str++; + len--; + } + + return str; +} + diff --git a/core/sm_stringutil.h b/core/sm_stringutil.h index 08e379b6..e96aac62 100644 --- a/core/sm_stringutil.h +++ b/core/sm_stringutil.h @@ -52,5 +52,6 @@ char *sm_strdup(const char *str); size_t CorePlayerTranslate(int client, char *buffer, size_t maxlength, const char *phrase, void **params); unsigned int UTIL_ReplaceAll(char *subject, size_t maxlength, const char *search, const char *replace); char *UTIL_ReplaceEx(char *subject, size_t maxLen, const char *search, size_t searchLen, const char *replace, size_t replaceLen); +char *UTIL_TrimWhitespace(char *str, size_t &len); #endif // _INCLUDE_SOURCEMOD_STRINGUTIL_H_ diff --git a/core/smn_filesystem.cpp b/core/smn_filesystem.cpp index 883ee5b8..b209bfd3 100644 --- a/core/smn_filesystem.cpp +++ b/core/smn_filesystem.cpp @@ -557,13 +557,6 @@ static cell_t sm_LogError(IPluginContext *pContext, const cell_t *params) return 1; } -enum -{ - FileTime_LastAccess = 0, /* Last access (not available on FAT) */ - FileTime_Created = 1, /* Creation (not available on FAT) */ - FileTime_LastChange = 2, /* Last modification */ -}; - static cell_t sm_GetFileTime(IPluginContext *pContext, const cell_t *params) { char *name; @@ -574,30 +567,16 @@ static cell_t sm_GetFileTime(IPluginContext *pContext, const cell_t *params) return 0; } + time_t time_val; char realpath[PLATFORM_MAX_PATH]; g_SourceMod.BuildPath(Path_Game, realpath, sizeof(realpath), "%s", name); -#ifdef PLATFORM_WINDOWS - struct _stat s; - if (_stat(realpath, &s) != 0) -#elif defined PLATFORM_POSIX - struct stat s; - if (stat(realpath, &s) != 0) -#endif + if (!g_LibSys.FileTime(realpath, (FileTimeType)params[2], &time_val)) { return -1; - } else { - if (params[2] == FileTime_LastAccess) - { - return (cell_t)s.st_atime; - } else if (params[2] == FileTime_Created) { - return (cell_t)s.st_ctime; - } else if (params[2] == FileTime_LastChange) { - return (cell_t)s.st_mtime; - } } - return -1; + return (cell_t)time_val; } static cell_t sm_LogToOpenFile(IPluginContext *pContext, const cell_t *params) diff --git a/core/smn_maplists.cpp b/core/smn_maplists.cpp new file mode 100644 index 00000000..6a50b974 --- /dev/null +++ b/core/smn_maplists.cpp @@ -0,0 +1,659 @@ +/** + * vim: set ts=4 : + * ============================================================================= + * SourceMod + * Copyright (C) 2004-2007 AlliedModders LLC. All rights reserved. + * ============================================================================= + * + * This program is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, version 3.0, as published by the + * Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + * details. + * + * You should have received a copy of the GNU General Public License along with + * this program. If not, see . + * + * As a special exception, AlliedModders LLC gives you permission to link the + * code of this program (as well as its derivative works) to "Half-Life 2," the + * "Source Engine," the "SourcePawn JIT," and any Game MODs that run on software + * by the Valve Corporation. You must obey the GNU General Public License in + * all respects for all other code used. Additionally, AlliedModders LLC grants + * this exception to all derivative works. AlliedModders LLC defines further + * exceptions, found in LICENSE.txt (as of this writing, version JULY-31-2007), + * or . + * + * Version: $Id$ + */ + +#include +#include "sm_globals.h" +#include "sm_trie_tpl.h" +#include "CellArray.h" +#include "convar.h" +#include "sourcemm_api.h" +#include "LibrarySys.h" +#include "TextParsers.h" +#include "sm_stringutil.h" +#include "sourcemod.h" +#include "Logger.h" +#include "HandleSys.h" + +using namespace SourceHook; + +struct maplist_info_t +{ + bool bIsCompat; + bool bIsPath; + char name[PLATFORM_MAX_PATH]; + char path[PLATFORM_MAX_PATH]; + time_t last_modified_time; + CellArray *pArray; + int serial; +}; + +#define MAPLIST_FLAG_MAPSFOLDER (1<<0) /**< On failure, use all maps in the maps folder. */ +#define MAPLIST_FLAG_CLEARARRAY (1<<1) /**< If an input array is specified, clear it before adding. */ +#define MAPLIST_FLAG_NO_DEFAULT (1<<2) /**< Do not read "default" or "mapcyclefile" on failure. */ + +class MapLists : public SMGlobalClass, public ITextListener_SMC +{ +public: + enum MapListState + { + MPS_NONE, + MPS_GLOBAL, + MPS_MAPLIST, + }; +public: + MapLists() + { + m_pMapCycleFile = NULL; + m_ConfigLastChanged = 0; + m_nSerialChange = 0; + } + void OnSourceModAllInitialized() + { + m_pMapCycleFile = icvar->FindVar("mapcyclefile"); + g_SourceMod.BuildPath(Path_SM, m_ConfigFile, sizeof(m_ConfigFile), "configs/maplists.cfg"); + } + void OnSourceModShutdown() + { + DumpCache(NULL); + } + void AddOrUpdateDefault(const char *name, const char *file) + { + char path[PLATFORM_MAX_PATH]; + maplist_info_t *pMapList, **ppMapList; + + if ((ppMapList = m_ListLookup.retrieve(name)) == NULL) + { + pMapList = new maplist_info_t; + pMapList->bIsCompat = true; + pMapList->bIsPath = true; + pMapList->last_modified_time = 0; + strncopy(pMapList->name, name, sizeof(pMapList->name)); + pMapList->pArray = NULL; + g_SourceMod.BuildPath(Path_Game, + pMapList->path, + sizeof(pMapList->path), + "%s", + file); + pMapList->serial = 0; + m_ListLookup.insert(name, pMapList); + m_MapLists.push_back(pMapList); + return; + } + + pMapList = *ppMapList; + + /* Don't modify if it's from the config file */ + if (!pMapList->bIsCompat) + { + return; + } + + g_SourceMod.BuildPath(Path_Game, + path, + sizeof(path), + "%s", + file); + + /* If the path matches, don't reset the serial/time */ + if (strcmp(path, pMapList->path) == 0) + { + return; + } + + strncopy(pMapList->path, path, sizeof(pMapList->path)); + pMapList->bIsPath = true; + pMapList->last_modified_time = 0; + pMapList->serial = 0; + } + void UpdateCache() + { + bool fileFound; + SMCError error; + time_t fileTime; + SMCStates states = {0, 0}; + + fileFound = g_LibSys.FileTime(m_ConfigFile, FileTime_LastChange, &fileTime); + + /* If the file is found and hasn't changed, bail out now. */ + if (fileFound && fileTime == m_ConfigLastChanged) + { + return; + } + + /* If the file wasn't found, and we already have entries, we bail out too. + * This case lets us optimize when a user deletes the config file, so we + * don't reparse every single time the function is called. + */ + if (!fileFound && m_MapLists.size() > 0) + { + return; + } + + /* Dump everything we know about. */ + List compat; + DumpCache(&compat); + + /* All this is to add the default entry back in. */ + maplist_info_t *pDefList = new maplist_info_t; + + pDefList->bIsPath = true; + strncopy(pDefList->name, "mapcyclefile", sizeof(pDefList->name)); + g_SourceMod.BuildPath(Path_Game, + pDefList->path, + sizeof(pDefList->path), + "%s", + m_pMapCycleFile ? m_pMapCycleFile->GetString() : "mapcycle.txt"); + pDefList->last_modified_time = 0; + pDefList->pArray = NULL; + pDefList->serial = 0; + + m_ListLookup.insert("mapcyclefile", pDefList); + m_MapLists.push_back(pDefList); + + /* Now parse the config file even if we don't know about it. + * This will give us a nice error message. + */ + if ((error = g_TextParser.ParseFile_SMC(m_ConfigFile, this, &states)) + != SMCError_Okay) + { + const char *errmsg = g_TextParser.GetSMCErrorString(error); + if (errmsg == NULL) + { + errmsg = "Unknown error"; + } + g_Logger.LogError("[SM] Could not parse file \"%s\"", m_ConfigFile); + g_Logger.LogError("[SM] Error on line %d (col %d): %s", + states.line, + states.col, + errmsg); + } + else + { + m_ConfigLastChanged = fileTime; + } + + /* Now, re-add compat stuff back in if we can. */ + List::iterator iter = compat.begin(); + while (iter != compat.end()) + { + if (m_ListLookup.retrieve((*iter)->name) != NULL) + { + /* The compatibility shim is no longer needed. */ + delete (*iter)->pArray; + delete (*iter); + } + else + { + m_ListLookup.insert((*iter)->name, (*iter)); + m_MapLists.push_back((*iter)); + } + iter = compat.erase(iter); + } + } + void ReadSMC_ParseStart() + { + m_CurState = MPS_NONE; + m_IgnoreLevel = 0; + m_pCurMapList = NULL; + } + SMCResult ReadSMC_NewSection(const SMCStates *states, const char *name) + { + if (m_IgnoreLevel) + { + m_IgnoreLevel++; + return SMCResult_Continue; + } + + if (m_CurState == MPS_NONE) + { + if (strcmp(name, "MapLists") == 0) + { + m_CurState = MPS_GLOBAL; + } + else + { + m_IgnoreLevel = 1; + } + } + else if (m_CurState == MPS_GLOBAL) + { + m_pCurMapList = new maplist_info_t; + + memset(m_pCurMapList, 0, sizeof(maplist_info_t)); + strncopy(m_pCurMapList->name, name, sizeof(m_pCurMapList->name)); + + m_CurState = MPS_MAPLIST; + } + else if (m_CurState == MPS_MAPLIST) + { + m_IgnoreLevel++; + } + + return SMCResult_Continue; + } + SMCResult ReadSMC_KeyValue(const SMCStates *states, const char *key, const char *value) + { + if (m_IgnoreLevel || m_pCurMapList == NULL) + { + return SMCResult_Continue; + } + + if (strcmp(key, "file") == 0) + { + g_SourceMod.BuildPath(Path_Game, + m_pCurMapList->path, + sizeof(m_pCurMapList->path), + "%s", + value); + m_pCurMapList->bIsPath = true; + } + else if (strcmp(key, "target") == 0) + { + strncopy(m_pCurMapList->path, value, sizeof(m_pCurMapList->path)); + m_pCurMapList->bIsPath = false; + } + + return SMCResult_Continue; + } + SMCResult ReadSMC_LeavingSection(const SMCStates *states) + { + if (m_IgnoreLevel) + { + m_IgnoreLevel--; + return SMCResult_Continue; + } + + if (m_CurState == MPS_MAPLIST) + { + if (m_pCurMapList != NULL + && m_pCurMapList->path[0] != '\0' + && m_ListLookup.retrieve(m_pCurMapList->name) == NULL) + { + m_ListLookup.insert(m_pCurMapList->name, m_pCurMapList); + m_MapLists.push_back(m_pCurMapList); + m_pCurMapList = NULL; + } + else + { + delete m_pCurMapList; + m_pCurMapList = NULL; + } + m_CurState = MPS_GLOBAL; + } + else if (m_CurState == MPS_GLOBAL) + { + m_CurState = MPS_NONE; + } + + return SMCResult_Continue; + } + void ReadSMC_ParseEnd(bool halted, bool failed) + { + delete m_pCurMapList; + m_pCurMapList = NULL; + } + static int sort_maps_in_adt_array(const void *str1, const void *str2) + { + return strcmp((char *)str1, (char *)str2); + } + CellArray *UpdateMapList(CellArray *pUseArray, const char *name, int *pSerial, unsigned int flags) + { + int change_serial; + CellArray *pNewArray; + bool success, free_new_array; + + free_new_array = false; + + if ((success = GetMapList(&pNewArray, name, &change_serial)) == false) + { + if ((flags & MAPLIST_FLAG_NO_DEFAULT) != MAPLIST_FLAG_NO_DEFAULT) + { + /* If this list failed, and it's not the default, try the default. + */ + if (strcmp(name, "default") != 0) + { + success = GetMapList(&pNewArray, name, &change_serial); + } + /* If either of the last two conditions failed, try again if we can. */ + if (!success) + { + success = GetMapList(&pNewArray, "mapcyclefile", &change_serial); + } + } + } + + /* If there was a success, and the serial has not changed, bail out. */ + if (*pSerial == change_serial) + { + return NULL; + } + + /** + * If there was a success but no map list, we need to look in the maps folder. + * If there was a failure and the flag is specified, we need to look in the maps folder. + */ + if ((success && pNewArray == NULL) + || (!success && ((flags & MAPLIST_FLAG_MAPSFOLDER) == MAPLIST_FLAG_MAPSFOLDER))) + { + char path[255]; + IDirectory *pDir; + + pNewArray = new CellArray(64); + free_new_array = true; + g_SourceMod.BuildPath(Path_Game, path, sizeof(path), "maps"); + + if ((pDir = g_LibSys.OpenDirectory(path)) != NULL) + { + char *ptr; + cell_t *blk; + char buffer[PLATFORM_MAX_PATH]; + + while (pDir->MoreFiles()) + { + if (!pDir->IsEntryFile() + || strcmp(pDir->GetEntryName(), ".") == 0 + || strcmp(pDir->GetEntryName(), "..") == 0) + { + pDir->NextEntry(); + continue; + } + strncopy(buffer, pDir->GetEntryName(), sizeof(buffer)); + if ((ptr = strstr(buffer, ".bsp")) == NULL) + { + pDir->NextEntry(); + continue; + } + *ptr = '\0'; + if (!engine->IsMapValid(buffer)) + { + pDir->NextEntry(); + continue; + } + if ((blk = pNewArray->push()) == NULL) + { + pDir->NextEntry(); + continue; + } + strncopy((char *)blk, buffer, 255); + pDir->NextEntry(); + } + g_LibSys.CloseDirectory(pDir); + } + + /* Remove the array if there were no items. */ + if (pNewArray->size() == 0) + { + delete pNewArray; + pNewArray = NULL; + } + else + { + qsort(pNewArray->base(), + pNewArray->size(), + pNewArray->blocksize() * sizeof(cell_t), + sort_maps_in_adt_array); + } + + change_serial = -1; + } + + /* If there is still no array by this point, bail out. */ + if (pNewArray == NULL) + { + *pSerial = -1; + return NULL; + } + + *pSerial = change_serial; + + /* If there is no input array, return something temporary. */ + if (pUseArray == NULL) + { + if (free_new_array) + { + return pNewArray; + } + else + { + return pNewArray->clone(); + } + } + + /* Clear the input array if necessary. */ + if ((flags & MAPLIST_FLAG_CLEARARRAY) == MAPLIST_FLAG_CLEARARRAY) + { + pUseArray->clear(); + } + + /* Copy. */ + cell_t *blk_dst; + cell_t *blk_src; + for (size_t i = 0; i < pNewArray->size(); i++) + { + blk_dst = pUseArray->push(); + blk_src = pNewArray->at(i); + strncopy((char *)blk_dst, (char *)blk_src, pUseArray->blocksize() * sizeof(cell_t)); + } + + /* Free resources if necessary. */ + if (free_new_array) + { + delete pNewArray; + } + + /* Return the array we were given. */ + return pUseArray; + } +private: + bool GetMapList(CellArray **ppArray, const char *name, int *pSerial) + { + time_t last_time; + maplist_info_t *pMapList, **ppMapList; + + if ((ppMapList = m_ListLookup.retrieve(name)) == NULL) + { + return false; + } + + pMapList = *ppMapList; + + if (!pMapList->bIsPath) + { + return GetMapList(ppArray, pMapList->path, pSerial); + } + + /* If it is a path, and the path is "*", assume all files must be used. */ + if (strcmp(pMapList->path, "*") == 0) + { + *ppArray = NULL; + return true; + } + + if (m_pMapCycleFile != NULL && strcmp(name, "mapcyclefile") == 0) + { + if (strcmp(m_pMapCycleFile->GetString(), pMapList->path) != 0) + { + strncopy(pMapList->path, m_pMapCycleFile->GetString(), sizeof(pMapList->path)); + pMapList->last_modified_time = 0; + } + } + + if (!g_LibSys.FileTime(pMapList->path, FileTime_LastChange, &last_time) + || last_time > pMapList->last_modified_time) + { + /* Reparse */ + FILE *fp; + cell_t *blk; + char buffer[255]; + + if ((fp = fopen(pMapList->path, "rt")) == NULL) + { + return false; + } + + delete pMapList->pArray; + pMapList->pArray = new CellArray(64); + + while (!feof(fp) && fgets(buffer, sizeof(buffer), fp) != NULL) + { + size_t len = strlen(buffer); + char *ptr = UTIL_TrimWhitespace(buffer, len); + if (*ptr == '\0' + || *ptr == ';' + || strncmp(ptr, "//", 2) == 0) + { + continue; + } + if (!engine->IsMapValid(ptr)) + { + continue; + } + if ((blk = pMapList->pArray->push()) != NULL) + { + strncopy((char *)blk, ptr, 255); + } + } + + fclose(fp); + + pMapList->last_modified_time = last_time; + pMapList->serial = ++m_nSerialChange; + } + + if (pMapList->pArray == NULL || pMapList->pArray->size() == 0) + { + return false; + } + + *pSerial = pMapList->serial; + *ppArray = pMapList->pArray; + + return true; + } + void DumpCache(List *compat_list) + { + m_ListLookup.clear(); + + List::iterator iter = m_MapLists.begin(); + while (iter != m_MapLists.end()) + { + if (compat_list != NULL && (*iter)->bIsCompat) + { + compat_list->push_back((*iter)); + } + else + { + delete (*iter)->pArray; + delete (*iter); + } + iter = m_MapLists.erase(iter); + } + } +private: + char m_ConfigFile[PLATFORM_MAX_PATH]; + time_t m_ConfigLastChanged; + ConVar *m_pMapCycleFile; + KTrie m_ListLookup; + List m_MapLists; + MapListState m_CurState; + unsigned int m_IgnoreLevel; + maplist_info_t *m_pCurMapList; + int m_nSerialChange; +} s_MapLists; + +static cell_t LoadMapList(IPluginContext *pContext, const cell_t *params) +{ + char *str; + Handle_t hndl; + cell_t *addr, flags; + CellArray *pArray, *pNewArray; + + hndl = params[1]; + pContext->LocalToPhysAddr(params[2], &addr); + pContext->LocalToString(params[3], &str); + flags = params[4]; + + /* Make sure the input Handle is valid */ + pArray = NULL; + if (hndl != BAD_HANDLE) + { + HandleError err; + HandleSecurity sec(pContext->GetIdentity(), g_pCoreIdent); + + if ((err = g_HandleSys.ReadHandle(hndl, htCellArray, &sec, (void **)&pArray)) + != HandleError_None) + { + return pContext->ThrowNativeError("Invalid Handle %x (error %d)", hndl, err); + } + } + + /* Make sure the map list cache is up to date at the root */ + s_MapLists.UpdateCache(); + + /* Try to get the map list. */ + if ((pNewArray = s_MapLists.UpdateMapList(pArray, str, addr, flags)) == NULL) + { + return BAD_HANDLE; + } + + /* If the user wanted a new array, create it now. */ + if (hndl == BAD_HANDLE) + { + if ((hndl = g_HandleSys.CreateHandle(htCellArray, pNewArray, pContext->GetIdentity(), g_pCoreIdent, NULL)) + == BAD_HANDLE) + { + *addr = -1; + delete pNewArray; + return BAD_HANDLE; + } + } + + return hndl; +} + +static cell_t SetMapListCompatBind(IPluginContext *pContext, const cell_t *params) +{ + char *name, *file; + + pContext->LocalToString(params[1], &name); + pContext->LocalToString(params[2], &file); + + s_MapLists.AddOrUpdateDefault(name, file); + + return 1; +} + +REGISTER_NATIVES(mapListNatives) +{ + {"ReadMapList", LoadMapList}, + {"SetMapListCompatBind", SetMapListCompatBind}, + {NULL, NULL}, +}; diff --git a/core/systems/LibrarySys.cpp b/core/systems/LibrarySys.cpp index 373fe223..6b067217 100644 --- a/core/systems/LibrarySys.cpp +++ b/core/systems/LibrarySys.cpp @@ -29,6 +29,7 @@ * Version: $Id$ */ +#include #include #include #include @@ -396,3 +397,32 @@ size_t LibrarySystem::GetFileFromPath(char *buffer, size_t maxlength, const char /* We scanned and found no path separator */ return UTIL_Format(buffer, maxlength, "%s", path); } + +bool LibrarySystem::FileTime(const char *path, FileTimeType type, time_t *pTime) +{ +#ifdef PLATFORM_WINDOWS + struct _stat s; + if (_stat(path, &s) != 0) +#elif defined PLATFORM_POSIX + struct stat s; + if (stat(path, &s) != 0) +#endif + { + return false; + } + + if (type == FileTime_LastAccess) + { + *pTime = s.st_atime; + } + else if (type == FileTime_Created) + { + *pTime = s.st_ctime; + } + else if (type == FileTime_LastChange) + { + *pTime = s.st_mtime; + } + + return true; +} diff --git a/core/systems/LibrarySys.h b/core/systems/LibrarySys.h index c1be68c1..cc7aa121 100644 --- a/core/systems/LibrarySys.h +++ b/core/systems/LibrarySys.h @@ -94,6 +94,7 @@ public: const char *GetFileExtension(const char *filename); bool CreateFolder(const char *path); size_t GetFileFromPath(char *buffer, size_t maxlength, const char *path); + bool FileTime(const char *path, FileTimeType type, time_t *pTime); }; extern LibrarySystem g_LibSys; diff --git a/plugins/basecommands.sp b/plugins/basecommands.sp index f85c0ee0..6f4f765d 100644 --- a/plugins/basecommands.sp +++ b/plugins/basecommands.sp @@ -49,7 +49,6 @@ public Plugin:myinfo = new Handle:hTopMenu = INVALID_HANDLE; new Handle:g_MapList; -new g_mapFileTime; #include "basecommands/kick.sp" #include "basecommands/reloadadmins.sp" @@ -82,6 +81,10 @@ public OnPluginStart() g_MapList = CreateMenu(MenuHandler_ChangeMap); SetMenuTitle(g_MapList, "Please select a map"); SetMenuExitBackButton(g_MapList, true); + + decl String:mapListPath[PLATFORM_MAX_PATH]; + BuildPath(Path_SM, mapListPath, sizeof(mapListPath), "configs/adminmenu_maplist.ini"); + SetMapListCompatBind("admin menu", mapListPath); } public OnMapStart() diff --git a/plugins/basecommands/map.sp b/plugins/basecommands/map.sp index 45b00ad9..0bf462b5 100644 --- a/plugins/basecommands/map.sp +++ b/plugins/basecommands/map.sp @@ -82,109 +82,37 @@ public Action:Timer_ChangeMap(Handle:timer, Handle:dp) return Plugin_Stop; } +new Handle:g_map_array = INVALID_HANDLE; +new g_map_serial = -1; + LoadMapList(Handle:menu) { - decl String:mapPath[256]; - BuildPath(Path_SM, mapPath, sizeof(mapPath), "configs/adminmenu_maplist.ini"); + new Handle:map_array; - if (!FileExists(mapPath)) - { - if (g_MapList != INVALID_HANDLE) - { - RemoveAllMenuItems(menu); - } - - return LoadMapFolder(menu); - } - - // If the file hasn't changed, there's no reason to reload - // all of the maps. - new fileTime = GetFileTime(mapPath, FileTime_LastChange); - if (g_mapFileTime == fileTime) + if ((map_array = ReadMapList(g_map_array, + g_map_serial, + "admin menu", + MAPLIST_FLAG_CLEARARRAY|MAPLIST_FLAG_NO_DEFAULT|MAPLIST_FLAG_MAPSFOLDER)) + != INVALID_HANDLE) { - return GetMenuItemCount(menu); + g_map_array = map_array; } - g_mapFileTime = fileTime; - - // Reset the array - if (g_MapList != INVALID_HANDLE) + if (g_map_array == INVALID_HANDLE) { - RemoveAllMenuItems(menu); - } - - new Handle:file = OpenFile(mapPath, "rt"); - if (file == INVALID_HANDLE) - { - LogError("[SM] Could not open file: %s, reverting to map folder", mapPath); - return LoadMapFolder(menu); - } - - decl String:buffer[256], len; - while (!IsEndOfFile(file) && ReadFileLine(file, buffer, sizeof(buffer))) - { - TrimString(buffer); - - if ((len = StrContains(buffer, ".bsp", false)) != -1) - { - buffer[len] = '\0'; - } - - if (buffer[0] == '\0' - || buffer[0] == ';' - || buffer[0] == '/' - || !IsValidConVarChar(buffer[0])) - { - continue; - } - - if (IsMapValid(buffer)) - { - AddMenuItem(menu, buffer, buffer); - } - } - - CloseHandle(file); - - new count = GetMenuItemCount(menu); - - if (!count) - return LoadMapFolder(menu); - else - return count; -} - -LoadMapFolder(Handle:menu) -{ - LogMessage("[SM] Loading menu map list from maps folder"); - - new Handle:mapDir = OpenDirectory("maps/"); - - if (mapDir == INVALID_HANDLE) - { - LogError("[SM] Could not open map directory for reading"); return 0; } - new String:mapName[64]; - new String:buffer[64]; - new FileType:fileType; - new len; + RemoveAllMenuItems(menu); - while(ReadDirEntry(mapDir, mapName, sizeof(mapName), fileType)) + decl String:map_name[64]; + new map_count = GetArraySize(g_map_array); + + for (new i = 0; i < map_count; i++) { - if(fileType == FileType_File) - { - len = strlen(mapName); - - if(SplitString(mapName, ".bsp", buffer, sizeof(buffer)) == len) - { - AddMenuItem(menu, buffer, buffer); - } - } - } - - CloseHandle(mapDir); - - return GetMenuItemCount(menu); + GetArrayString(g_map_array, i, map_name, sizeof(map_name)); + AddMenuItem(menu, map_name, map_name); + } + + return map_count; } diff --git a/plugins/include/sorting.inc b/plugins/include/sorting.inc index 6d49a238..9cb395d6 100644 --- a/plugins/include/sorting.inc +++ b/plugins/include/sorting.inc @@ -48,8 +48,8 @@ enum SortOrder /** * Data types for ADT Array Sorts */ - enum SortType - { +enum SortType +{ Sort_Integer = 0, Sort_Float, Sort_String, @@ -147,7 +147,7 @@ native SortCustom2D(array[][], array_size, SortFunc2D:sortfunc, Handle:hndl=INVA * @param type Data type stored in the ADT Array * @noreturn */ -native SortADTArray(Handle:array, SortOrder:order = Sort_Ascending, SortType:type = Sort_Integer); +native SortADTArray(Handle:array, SortOrder:order, SortType:type); /** * Sort comparison function for ADT Array elements. Function provides you with diff --git a/plugins/include/sourcemod.inc b/plugins/include/sourcemod.inc index acbcdeca..f10e9836 100644 --- a/plugins/include/sourcemod.inc +++ b/plugins/include/sourcemod.inc @@ -490,6 +490,70 @@ forward OnLibraryAdded(const String:name[]); */ forward OnLibraryRemoved(const String:name[]); +#define MAPLIST_FLAG_MAPSFOLDER (1<<0) /**< On failure, use all maps in the maps folder. */ +#define MAPLIST_FLAG_CLEARARRAY (1<<1) /**< If an input array is specified, clear it before adding. */ +#define MAPLIST_FLAG_NO_DEFAULT (1<<2) /**< Do not read "default" or "mapcyclefile" on failure. */ + +/** + * Loads a map list to an ADT Array. + * + * A map list is a list of maps from a file. SourceMod allows easy configuration of + * maplists through addons/sourcemod/configs/maplists.cfg. Each entry is given a + * name and a file (for example, "rtv" => "rtv.cfg"), or a name and a redirection + * (for example, "rtv" => "default"). This native will read a map list entry, + * cache the file, and return the list of maps it holds. + * + * Serial change numbers are used to identify if a map list has changed. Thus, if + * you pass a serial change number and it's equal to what SourceMod currently knows + * about the map list, then SourceMod won't reparse the file. + * + * If the maps end up being read from the maps folder (MAPLIST_FLAG_MAPSFOLDER), they + * are automatically sorted in alphabetical, ascending order. + * + * @param array Array to store the map list. If INVALID_HANDLE, a new blank + * array will be created. The blocksize should be at least 16; + * otherwise results may be truncated. Items are added to the array + * as strings. The array is never checked for duplicates, and it is + * not read beforehand. Only the serial number is used to detect + * changes. + * @param str Config name, or "default" for the default map list. Config names + * should be somewhat descriptive. For example, the admin menu uses + * a config name of "admin menu". The list names can be configured + * by users in addons/sourcemod/configs/maplists.cfg. + * @param serial Serial number to identify last known map list change. If -1, the + * the value will not be checked. If the map list has since changed, + * the serial is updated (even if -1 was passed). If there is an error + * finding a valid maplist, then the serial is set to -1. + * @param flags MAPLIST_FLAG flags. + * @return On failure: + * INVALID_HANDLE is returned, the serial is set to -1, and the input + * array (if any) is left unchanged. + * On no change: + INVALID_HANDLE is returned, the serial is unchanged, and the input + array (if any) is left unchanged. + * On success: + * A valid array Handle is returned, containing at least one map string. + * If an array was passed, the return value is equal to the passed Array + * Handle. If the passed array was not cleared, it will have grown by at + * least one item. The serial number is updated to a positive number. + * @error Invalid array Handle that is not INVALID_HANDLE. + */ +native Handle:ReadMapList(Handle:array=INVALID_HANDLE, + &serial=-1, + const String:str[]="default", + flags=MAPLIST_FLAG_CLEARARRAY); + +/** + * Makes a compatibility binding for map lists. For example, if a function previously used + * "clam.cfg" for map lists, this function will insert a "fake" binding to "clam.cfg" that + * will be overridden if it's in the maplists.cfg file. + * + * @param name Configuration name that would be used with ReadMapList(). + * @param file Default file to use. + * @noreturn + */ +native SetMapListCompatBind(const String:name[], const String:file[]); + #include #include diff --git a/public/ILibrarySys.h b/public/ILibrarySys.h index e2cc240c..14dd22c1 100644 --- a/public/ILibrarySys.h +++ b/public/ILibrarySys.h @@ -42,7 +42,14 @@ namespace SourceMod { #define SMINTERFACE_LIBRARYSYS_NAME "ILibrarySys" - #define SMINTERFACE_LIBRARYSYS_VERSION 3 + #define SMINTERFACE_LIBRARYSYS_VERSION 4 + + enum FileTimeType + { + FileTime_LastAccess = 0, /* Last access (not available on FAT) */ + FileTime_Created = 1, /* Creation (not available on FAT) */ + FileTime_LastChange = 2, /* Last modification */ + }; class ILibrary { @@ -202,6 +209,19 @@ namespace SourceMod * @return True on success, false otherwise. */ virtual bool CreateFolder(const char *path) =0; + + /** + * @brief Returns the requested timestamp of a file. + * + * NOTE: On FAT file systems, the access and creation times + * may not be valid. + * + * @param path Path to file. + * @param type FileTimeType of time value to request. + * @param pTime Pointer to store time. + * @return True on success, false on failure. + */ + virtual bool FileTime(const char *path, FileTimeType type, time_t *pTime) =0; }; }