diff --git a/amxmodx/meta_api.cpp b/amxmodx/meta_api.cpp index b068eb6d..e4dc87f8 100755 --- a/amxmodx/meta_api.cpp +++ b/amxmodx/meta_api.cpp @@ -417,6 +417,7 @@ int C_Spawn(edict_t *pent) ArrayHandles.clear(); TrieHandles.clear(); + TrieIterHandles.clear(); TrieSnapshotHandles.clear(); DataPackHandles.clear(); TextParsersHandles.clear(); diff --git a/amxmodx/natives_handles.h b/amxmodx/natives_handles.h index 530d1faf..f031e065 100644 --- a/amxmodx/natives_handles.h +++ b/amxmodx/natives_handles.h @@ -45,6 +45,11 @@ class NativeHandle m_handles.clear(); } + size_t size() + { + return m_handles.length(); + } + T *lookup(int handle) { --handle; diff --git a/amxmodx/trie_natives.cpp b/amxmodx/trie_natives.cpp index 713a9bdf..d22e0cc3 100644 --- a/amxmodx/trie_natives.cpp +++ b/amxmodx/trie_natives.cpp @@ -10,6 +10,7 @@ #include "trie_natives.h" NativeHandle TrieHandles; +NativeHandle TrieIterHandles; NativeHandle TrieSnapshotHandles; // native Trie:TrieCreate(); @@ -344,6 +345,18 @@ static cell AMX_NATIVE_CALL TrieDestroy(AMX *amx, cell *params) return 0; } + CellTrieIter *iter; + for (size_t index = 1; index <= TrieIterHandles.size(); index++) + { + if ((iter = TrieIterHandles.lookup(index))) + { + if (iter->trie == t) + { + iter->trie = nullptr; + } + } + } + if (TrieHandles.destroy(*ptr)) { *ptr = 0; @@ -469,6 +482,226 @@ static cell AMX_NATIVE_CALL TrieSnapshotDestroy(AMX *amx, cell *params) return 0; } + +#define CHECK_ITER_HANDLE(handle) \ + if (!handle) { \ + LogError(amx, AMX_ERR_NATIVE, "Invalid map iterator handle provided (%d)", params[arg_handle]); \ + return 0; \ + } \ + if (!handle->trie) { \ + LogError(amx, AMX_ERR_NATIVE, "Closed map iterator handle provided (%d)", params[arg_handle]); \ + return 0; \ + } \ + if (handle->mod_count != handle->trie->map.mod_count()) { \ + LogError(amx, AMX_ERR_NATIVE, "Outdated map iterator handle provided (%d)", params[arg_handle]);\ + return 0; \ + } + +// native TrieIter:TrieIterCreate(Trie:handle) +static cell AMX_NATIVE_CALL TrieIterCreate(AMX *amx, cell *params) +{ + enum args { arg_count, arg_handle }; + + auto handle = TrieHandles.lookup(params[arg_handle]); + + if (!handle) + { + LogError(amx, AMX_ERR_NATIVE, "Invalid map handle provided (%d)", params[arg_handle]); + return 0; + } + + auto index = TrieIterHandles.create(handle); + auto iter = TrieIterHandles.lookup(index); + + return static_cast(index); +} + +// native bool:TrieIterEnded(TrieIter:handle) +static cell AMX_NATIVE_CALL TrieIterEnded(AMX *amx, cell *params) +{ + enum args { arg_count, arg_handle }; + + auto handle = TrieIterHandles.lookup(params[arg_handle]); + + CHECK_ITER_HANDLE(handle) + + return handle->iter.empty(); +} + +// native TrieIterNext(TrieIter:handle) +static cell AMX_NATIVE_CALL TrieIterNext(AMX *amx, cell *params) +{ + enum args { arg_count, arg_handle }; + + auto handle = TrieIterHandles.lookup(params[arg_handle]); + + CHECK_ITER_HANDLE(handle) + + if (handle->iter.empty()) + { + return 0; + } + + handle->iter.next(); + + return 1; +} + +// native TrieIterGetKey(TrieIter:handle, key[], outputsize) +static cell AMX_NATIVE_CALL TrieIterGetKey(AMX *amx, cell *params) +{ + enum args { arg_count, arg_handle, arg_output, arg_outputsize }; + + auto handle = TrieIterHandles.lookup(params[arg_handle]); + + CHECK_ITER_HANDLE(handle) + + auto& iter = handle->iter; + + if (iter.empty()) + { + *get_amxaddr(amx, params[arg_output]) = '\0'; + return 0; + } + + return set_amxstring_utf8(amx, params[arg_output], iter->key.chars(), iter->key.length(), params[arg_outputsize]); +} + +// native TrieIterGetSize(TrieIter:handle) +static cell AMX_NATIVE_CALL TrieIterGetSize(AMX *amx, cell *params) +{ + enum args { arg_count, arg_handle }; + + auto handle = TrieIterHandles.lookup(params[arg_handle]); + + CHECK_ITER_HANDLE(handle) + + return handle->trie->map.elements(); +} + +// native bool:TrieIterGetCell(TrieIter:handle, &any:value) +static cell AMX_NATIVE_CALL TrieIterGetCell(AMX *amx, cell *params) +{ + enum args { arg_count, arg_handle, arg_outputvalue }; + + auto handle = TrieIterHandles.lookup(params[arg_handle]); + + CHECK_ITER_HANDLE(handle) + + auto& iter = handle->iter; + + if (iter.empty() || !iter->value.isCell()) + { + return false; + } + + *get_amxaddr(amx, params[arg_outputvalue]) = iter->value.cell_(); + + return true; +} + +// native bool:TrieIterGetString(TrieIter:handle, buffer[], outputsize, &size = 0) +static cell AMX_NATIVE_CALL TrieIterGetString(AMX *amx, cell *params) +{ + enum args { arg_count, arg_handle, arg_output, arg_outputsize, arg_refsize }; + + auto handle = TrieIterHandles.lookup(params[arg_handle]); + + CHECK_ITER_HANDLE(handle) + + if (params[arg_outputsize] < 0) + { + LogError(amx, AMX_ERR_NATIVE, "Invalid buffer size (%d)", params[arg_outputsize]); + return 0; + } + + auto& iter = handle->iter; + + if (iter.empty() || !iter->value.isString()) + { + return false; + } + + auto refsize = get_amxaddr(amx, params[arg_refsize]); + + *refsize = set_amxstring_utf8(amx, params[arg_output], iter->value.chars(), strlen(iter->value.chars()), params[arg_outputsize]); + + return true; +} + +// native bool:TrieIterGetArray(TrieIter:handle, array[], outputsize, &size = 0) +static cell AMX_NATIVE_CALL TrieIterGetArray(AMX *amx, cell *params) +{ + enum args { arg_count, arg_handle, arg_output, arg_outputsize, arg_refsize }; + + auto handle = TrieIterHandles.lookup(params[arg_handle]); + + CHECK_ITER_HANDLE(handle) + + auto outputSize = params[arg_outputsize]; + + if (outputSize < 0) + { + LogError(amx, AMX_ERR_NATIVE, "Invalid array size (%d)", params[arg_outputsize]); + return 0; + } + + auto& iter = handle->iter; + + if (iter.empty() || !iter->value.isArray()) + { + return false; + } + + auto pOutput = get_amxaddr(amx, params[arg_output]); + auto pSize = get_amxaddr(amx, params[arg_refsize]); + + if (!iter->value.array() || !outputSize) + { + *pSize = 0; + return false; + } + + auto length = iter->value.arrayLength(); + auto base = iter->value.array(); + + if (length > size_t(outputSize)) + { + length = outputSize; + } + + *pSize = length; + + memcpy(pOutput, base, sizeof(cell) * length); + + return true; +} + +// native TrieIterDestroy(&TrieIter:handle) +static cell AMX_NATIVE_CALL TrieIterDestroy(AMX *amx, cell *params) +{ + enum args { arg_count, arg_handle }; + + auto refhandle = get_amxaddr(amx, params[arg_handle]); + auto handle = TrieIterHandles.lookup(*refhandle); + + if (!handle) + { + return false; + } + + handle->trie = nullptr; + + if (TrieIterHandles.destroy(*refhandle)) + { + *refhandle = 0; + return true; + } + + return false; +} + + AMX_NATIVE_INFO trie_Natives[] = { { "TrieCreate" , TrieCreate }, @@ -493,6 +726,16 @@ AMX_NATIVE_INFO trie_Natives[] = { "TrieSnapshotGetKey" , TrieSnapshotGetKey }, { "TrieSnapshotDestroy" , TrieSnapshotDestroy }, + { "TrieIterCreate" , TrieIterCreate }, + { "TrieIterEnded" , TrieIterEnded }, + { "TrieIterNext" , TrieIterNext }, + { "TrieIterGetKey" , TrieIterGetKey }, + { "TrieIterGetSize" , TrieIterGetSize }, + { "TrieIterGetCell" , TrieIterGetCell }, + { "TrieIterGetString" , TrieIterGetString }, + { "TrieIterGetArray" , TrieIterGetArray }, + { "TrieIterDestroy" , TrieIterDestroy }, + { nullptr , nullptr} }; diff --git a/amxmodx/trie_natives.h b/amxmodx/trie_natives.h index 695d72a5..c600b14d 100644 --- a/amxmodx/trie_natives.h +++ b/amxmodx/trie_natives.h @@ -14,6 +14,7 @@ #include #include #include "natives_handles.h" +#include enum EntryType { @@ -138,9 +139,128 @@ private: cell data_; }; + +template +class StringHashMapCustom +{ + typedef StringHashMap Internal; + +public: + StringHashMapCustom() : mod_count_(0) + {} + +public: + typedef typename Internal::Result Result; + typedef typename Internal::Insert Insert; + typedef typename Internal::iterator iterator; + +public: + size_t elements() const + { + return internal_.elements(); + } + + iterator iter() + { + return internal_.iter(); + } + + bool contains(const char *aKey) + { + return internal_.contains(aKey); + } + + bool replace(const char *aKey, const T &value) + { + return internal_.contains(aKey, value); + } + + bool insert(const char *aKey, const T &value) + { + if (internal_.insert(aKey, value)) + { + mod_count_++; + return true; + } + return false; + } + + bool remove(const char *aKey) + { + if (internal_.remove(aKey)) + { + mod_count_++; + return true; + } + return false; + } + + void clear() + { + mod_count_++; + internal_.clear(); + } + + void remove(Result &r) + { + mod_count_++; + internal_.remove(r); + } + + Result find(const char *aKey) + { + return internal_.find(aKey); + } + + Insert findForAdd(const char *aKey) + { + return internal_.findForAdd(aKey); + } + + bool add(Insert &i, const char *aKey) + { + if (internal_.add(i, aKey)) + { + mod_count_++; + return true; + } + return false; + } + + bool add(Insert &i) + { + if (internal_.add(i)) + { + mod_count_++; + return true; + } + return false; + } + +public: + size_t mod_count() const + { + return mod_count_; + } + +private: + Internal internal_; + size_t mod_count_; +}; + struct CellTrie { - StringHashMap map; + StringHashMapCustom map; +}; + +struct CellTrieIter +{ + CellTrieIter(CellTrie *_trie) : trie(_trie), iter(_trie->map.iter()), mod_count(_trie->map.mod_count()) + {} + + CellTrie *trie; + StringHashMapCustom::iterator iter; + size_t mod_count; }; struct TrieSnapshot @@ -160,6 +280,7 @@ struct TrieSnapshot }; extern NativeHandle TrieHandles; +extern NativeHandle TrieIterHandles; extern NativeHandle TrieSnapshotHandles; extern AMX_NATIVE_INFO trie_Natives[]; diff --git a/plugins/include/celltrie.inc b/plugins/include/celltrie.inc index bcf0b757..cf69dfeb 100644 --- a/plugins/include/celltrie.inc +++ b/plugins/include/celltrie.inc @@ -27,6 +27,21 @@ enum Trie Invalid_Trie = 0 }; +/** + * Hash map iterator tag declaration + * + * @note The word "Trie" in this API is historical. As of AMX Mod X 1.8.3, + * tries have been internally replaced with hash tables, which have O(1) + * insertion time instead of O(n). + * @note Plugins are responsible for freeing all TrieIter handles they acquire. + * Failing to free handles will result in the plugin and AMXX leaking + * memory. + */ +enum TrieIter +{ + Invalid_TrieIter = 0 +} + /** * Hash map snapshot tag declaration * @@ -269,3 +284,129 @@ native TrieSnapshotGetKey(Snapshot:handle, index, buffer[], maxlength); * @return 1 on success, 0 if an invalid handle was passed in */ native TrieSnapshotDestroy(&Snapshot:handle); + + +/** + * Creates an iterator for a map. It provides iterative read-only access to the + * maps contents. + * + * @note Removing or adding keys to the underlying map will invalidate all its + * iterators. Updating values of existing keys is allowed and the changes + * will be immediately reflected in the iterator. + * @note Iterators are designed to be short-lived and not stored, and creating + * them is very cheap. Reading data from an iterator is just as fast as + * reading directly from the map. + * @note Just like in snapshots the keys are not sorted. + * + * @return New iterator handle, which must be freed via TrieIterDestroy(). + * @error Invalid Handle + */ +native TrieIter:TrieIterCreate(Trie:handle); + +/** + * Returns if the iterator has reached its end and no more data can be read. + * + * @param handle Iterator handle + * + * @return True if iterator has reached the end, false otherwise + * @error Invalid Handle + * Iterator has been closed (underlying map destroyed) + * Iterator is outdated + */ +native bool:TrieIterEnded(TrieIter:handle); + +/** + * Advances the iterator to the next key/value pair if one is available. + * + * @param handle Iterator handle + * + * @error Invalid Handle + * Iterator has been closed (underlying map destroyed) + * Iterator is outdated + */ +native TrieIterNext(TrieIter:handle); + +/** + * Retrieves the key the iterator currently points to. + * + * @param handle Iterator handle. + * @param key Buffer to store the current key in. + * @param outputsize Maximum size of string buffer. + * + * @return Nnumber of bytes written to the buffer + * @error Invalid handle + * Iterator has been closed (underlying map destroyed) + * Iterator is outdated + */ +native TrieIterGetKey(TrieIter:handle, key[], outputsize); + +/** + * Retrieves the number of elements in the underlying map. + * + * @note When used on a valid iterator this is exactly the same as calling TrieGetSize on the map directly. + * + * @param handle Iterator handle + * + * @return Number of elements in the map + * @error Invalid handle + * Iterator has been closed (underlying map destroyed) + * Iterator is outdated + */ +native TrieIterGetSize(TrieIter:handle); + +/** + * Retrieves a value at the current position of the iterator. + * + * @param handle Iterator handle + * @param value Variable to store value in + * + * @return True on success, false if the iterator is empty or the current + * value is an array or a string. + * @error Invalid handle + * Iterator has been closed (underlying map destroyed) + * Iterator is outdated + */ +native bool:TrieIterGetCell(TrieIter:handle, &any:value); + +/** + * Retrieves a string at the current position of the iterator. + * + * @param handle Iterator handle + * @param buffer Buffer to store the string in + * @param outputsize Maximum size of string buffer + * @param size Optional parameter to store the number of bytes written to the buffer. + * + * @return True on success, false if the iterator is empty or the current value + * is not a string. + * @error Invalid handle + * Invalid buffer size + * Iterator has been closed (underlying map destroyed) + * Iterator is outdated + */ +native bool:TrieIterGetString(TrieIter:handle, buffer[], outputsize, &size = 0); + +/** + * Retrieves an array at the current position of the iterator. + * + * @param handle Iterator handle + * @param buffer Buffer to store the array + * @param outputsize Maximum size of buffer + * @param size Optional parameter to store the number of bytes written to the buffer + * + * @return True on success, false if the iterator is empty or the current + * value is not an array. + * @error Invalid handle + * Invalid buffer size + * Iterator has been closed (underlying map destroyed) + * Iterator is outdated + */ +native bool:TrieIterGetArray(TrieIter:handle, any:array[], outputsize, &size = 0); + +/** + * Destroys an iterator handle. + * + * @param handle Iterator handle. + * + * @return True on success, false if the value was never set. + */ +native TrieIterDestroy(&TrieIter:handle); diff --git a/plugins/testsuite/trietest.sma b/plugins/testsuite/trietest.sma index 06319b0d..ecbabcd2 100644 --- a/plugins/testsuite/trietest.sma +++ b/plugins/testsuite/trietest.sma @@ -197,7 +197,7 @@ public trietest() server_print("entry K42K should not be an array or string"); ok = false; } if (!TrieSetArray(t, "K42K", data, 1)) - { + { server_print("couldn't set K42K to 1-entry array"); ok = false; } if (!TrieGetArray(t, "K42K", array, sizeof(array), value)) @@ -262,7 +262,7 @@ public trietest() fail("Exists/Delete"); ok = true; - + TrieClear(t); if (TrieGetSize(t)) @@ -293,7 +293,7 @@ public trietest() else if (strcmp(buffer, "egg") == 0) found[2] = true; else { server_print("unexpected key: %s", buffer); ok = false; } } - + if (!found[0] || !found[1] || !found[2]) { server_print("did not find all keys"); ok = false; @@ -302,13 +302,240 @@ public trietest() TrieSnapshotDestroy(keys); TrieDestroy(t); - + if (ok) pass("Snapshot"); else fail("Snapshot"); + ok = true; + + t = TrieCreate(); + + TrieSetString(t, "full", "throttle"); + TrieSetString(t, "brutal", "legend"); + TrieSetString(t, "broken", "age"); + + new TrieIter:iter = TrieIterCreate(t); + { + if (TrieIterGetSize(iter) != 3) + { + server_print("Trie iterator size should be 3, is %d", TrieIterGetSize(iter)); + ok = false; + } + + if (TrieIterEnded(iter)) + { + server_print("Trie iterator should have a next key/value pair at this point"); + ok = false; + } + + new key[32], value[32], bool:valid[4], klen, vlen; + if (!TrieIterGetKey(iter, key, charsmax(key))) + { + server_print("Trie iterator should not be empty at this point (no key retrieval)"); + ok = false; + } + else + { + while (!TrieIterEnded(iter)) + { + klen = TrieIterGetKey(iter, key, charsmax(key)); + TrieIterGetString(iter, value, charsmax(value), vlen); + + if (strcmp(key, "full") == 0 && strcmp(value, "throttle") == 0) + valid[0] = true; + else if (strcmp(key, "brutal") == 0 && strcmp(value, "legend") == 0) + valid[1] = true; + else if (strcmp(key, "broken") == 0 && strcmp(value, "age") == 0) + valid[2] = true; + + if (strlen(key) != klen) + { + server_print("Key string length does not match. %d != %d", strlen(key), klen); + ok = false; + } + + if (strlen(value) != vlen) + { + server_print("Value string length does not match. %d != %d", strlen(key), vlen); + ok = false; + } + + // Should thrown an error + // TrieSetString(t, "monkey", "island"); + // TrieDeleteKey(t, "full"); + // TrieClear(t); + + TrieIterNext(iter); + } + + if (!valid[0] || !valid[1] || !valid[2]) + { + server_print("Did not find all value pairs (1)"); + ok = false; + } + } + + TrieIterDestroy(iter); + + if (iter) + { + server_print("Iterator handle should be null after being destroyed: %d", iter); + ok = false; + } + + iter = TrieIterCreate(t); + { + TrieSetString(t, "full", "speed"); + TrieSetString(t, "brutal", "uppercut"); + TrieSetString(t, "broken", "sword"); + + arrayset(valid, false, sizeof(valid)); + + for (; !TrieIterEnded(iter); TrieIterNext(iter)) + { + if (TrieIterGetCell(iter, value[0]) || TrieIterGetArray(iter, array, sizeof(array))) + { + server_print("Entries should not be an array or string"); + ok = false; + } + else + { + TrieIterGetKey(iter, key, charsmax(key)); + TrieIterGetString(iter, value, charsmax(value)); + + if (strcmp(key, "full") == 0) + TrieSetString(t, "full", "speed"); + else if (strcmp(key, "brutal") == 0) + TrieSetString(t, "brutal", "uppercut"); + else if (strcmp(key, "broken") == 0) + TrieSetString(t, "broken", "sword"); + } + } + + if (TrieGetString(t, "full", value, charsmax(value)) && strcmp(value, "speed") == 0) + valid[0] = true; + if (TrieGetString(t, "brutal", value, charsmax(value)) && strcmp(value, "uppercut") == 0) + valid[1] = true; + if (TrieGetString(t, "broken", value, charsmax(value)) && strcmp(value, "sword") == 0) + valid[2] = true; + + if (!valid[0] || !valid[1] || !valid[2]) + { + server_print("Did not set the new values (overwriting value is allowed)"); + ok = false; + } + + + } + TrieIterDestroy(iter); + + if (TrieIterDestroy(iter)) + { + server_print("Iter should be null"); + ok = false; + } + + iter = TrieIterCreate(t); + { + TrieDestroy(t); + // Should throw an error + // TrieIterNext(iter) + } + + if (!TrieIterDestroy(iter)) + { + server_print("Iter should be valid"); + ok = false; + } + + t = TrieCreate(); + TrieSetCell(t, "key_1", cellmin); + TrieSetArray(t, "key_2", data, sizeof data); + + iter = TrieIterCreate(t); + + if (TrieIterEnded(iter)) + { + server_print("Iter should not be ended: %d", iter); + ok = false; + } + + for (; !TrieIterEnded(iter); TrieIterNext(iter)) + { + TrieIterGetKey(iter, key, charsmax(key)); + + if (strcmp(key, "key_1") == 0) + { + new val; + if (!TrieIterGetCell(iter, val) || val != cellmin) + { + server_print("Failed to retrieve value (%d)", val) + ok = false; + } + } + else if (strcmp(key, "key_2") == 0) + { + if (!TrieIterGetArray(iter, array, sizeof(array))) + { + server_print("Failed to retrieve array") + ok = false; + } + else + { + for (new i = 0; i < sizeof data; i++) + { + if (data[i] != array[i]) + { + server_print("slot %d should be %d, got %d", i, data[i], array[i]); + ok = false; + } + } + } + } + } + + TrieClear(t); + TrieSetCell(t, "1", 1); + TrieSetCell(t, "2", 2); + TrieSetCell(t, "3", 3); + + new totalSum = 0; + new element1, element2; + new TrieIter:iter1, TrieIter:iter2; + + iter1 = TrieIterCreate(t) + for (; !TrieIterEnded(iter1); TrieIterNext(iter1)) + { + iter2 = TrieIterCreate(t); + for(; !TrieIterEnded(iter2); TrieIterNext(iter2)) + { + TrieIterGetCell(iter1, element1); + TrieIterGetCell(iter2, element2); + + totalSum += element1 * element2; + } + TrieIterDestroy(iter2); + } + TrieIterDestroy(iter1); + + if (totalSum != 36) + { + server_print("Sum should be 36, got %d", totalSum); + ok = false; + } + + TrieIterDestroy(iter); + TrieDestroy(t); + } + + if (ok) + pass("Iterator"); + else + fail("Iterator"); + done(); }