diff --git a/storm/String.cpp b/storm/String.cpp index 0b732f0..ebc61db 100644 --- a/storm/String.cpp +++ b/storm/String.cpp @@ -50,6 +50,13 @@ int32_t s_initialized; double s_realDigit[20][10]; +const uint32_t s_hashtable[16] = { + 0x486E26EE, 0xDCAA16B3, 0xE1918EEF, 0x202DAFDB, + 0x341C7DC7, 0x1C365303, 0x40EF2D37, 0x65FD5E49, + 0xD6057177, 0x904ECE93, 0x1C38024F, 0x98FD323B, + 0xE3061AE7, 0xA39B0FA1, 0x9797F25F, 0xE4444563, +}; + void GetNextTextUpper(uint32_t* orig, const char** string, uint32_t* upper) { uint8_t byte = **string; int32_t v3 = bytesFromUTF8[byte]; @@ -349,6 +356,42 @@ char* SStrDupA(const char* string, const char* filename, uint32_t linenumber) { return dup; } +uint32_t SStrHash(const char* string, uint32_t flags, uint32_t seed) { + STORM_VALIDATE_BEGIN; + STORM_VALIDATE(string); + STORM_VALIDATE_END; + + uint32_t result = seed ? seed : 0x7FED7FED; + uint32_t adjust = 0xEEEEEEEE; + uint32_t ch; + + if (flags & SSTR_HASH_CASESENSITIVE) { + for (; *string; string++) { + ch = *string; + + result = (s_hashtable[ch / 16] - s_hashtable[ch % 16]) ^ (adjust + result); + adjust = 33 * adjust + result + ch + 3; + } + } + else { + for (; *string; string++) { + ch = *string; + + if (ch >= 'a' && ch <= 'z') { + ch -= 32; + } + + if (ch == '/') { + ch = '\\'; + } + + result = (s_hashtable[ch / 16] - s_hashtable[ch % 16]) ^ (adjust + result); + adjust = 33 * adjust + result + ch + 3; + } + } + return result ? result : 1; +} + uint32_t SStrHashHT(const char* string) { char normalized[0x400]; char* buf = normalized; diff --git a/storm/String.hpp b/storm/String.hpp index 275d663..1a0971d 100644 --- a/storm/String.hpp +++ b/storm/String.hpp @@ -5,9 +5,13 @@ #include #include + #define STORM_MAX_PATH 260 #define STORM_MAX_STR 0x7FFFFFFF +#define SSTR_HASH_CASESENSITIVE 1 + + char* SStrChr(char* string, char search); const char* SStrChr(const char* string, char search); @@ -24,6 +28,8 @@ size_t SStrCopy(char* dest, const char* source, size_t destsize = STORM_MAX_STR) char* SStrDupA(const char* string, const char* filename, uint32_t linenumber); +uint32_t SStrHash(const char* string, uint32_t flags = 0, uint32_t seed = 0); + uint32_t SStrHashHT(const char* string); size_t SStrLen(const char* string); diff --git a/test/String.cpp b/test/String.cpp index 574b322..ba7f886 100644 --- a/test/String.cpp +++ b/test/String.cpp @@ -247,6 +247,107 @@ TEST_CASE("SStrDupA", "[string]") { } } +struct TestHash { + const char *str; + uint32_t hash; +}; + +TEST_CASE("SStrHash", "[string]") { + SECTION("hashes strings with case insensitivity") { + // Results obtained by directly calling Starcraft 1.17's SStrHash + auto testcase = GENERATE( + TestHash{ "bloop bloop", 0x3EB42E62 }, + TestHash{ "BLOOP bloop", 0x3EB42E62 }, + TestHash{ "Objects\\CameraHelper\\CameraHelper.mdx", 0xE99A51F5 }, + TestHash{ "Objects/CameraHelper/CameraHelper.mdx", 0xE99A51F5 }, + TestHash{ "abcdefghijklmnopqrstuvwxyz", 0x29564240 }, + TestHash{ "ABCDEFGHIJKLMNOPQRSTUVWXYZ", 0x29564240 }, + TestHash{ "123456789~!@#$%^&*()_+`-=[]{}|;':\",.<>?", 0x54E31141 }, + TestHash{ "/", 0x57EC56C7 }, + TestHash{ "\\", 0x57EC56C7 }, + TestHash{ "\r\n\t ", 0x89351E43 } + ); + + auto hash = SStrHash(testcase.str); + CHECK(hash == testcase.hash); + } + + SECTION("hashes strings with case sensitivity") { + // Results obtained by directly calling Starcraft 1.17's SStrHash + auto testcase = GENERATE( + TestHash{ "bloop bloop", 0x4E7D519E }, + TestHash{ "BLOOP bloop", 0x307459CC }, + TestHash{ "Objects\\CameraHelper\\CameraHelper.mdx", 0x5202C0C5 }, + TestHash{ "Objects/CameraHelper/CameraHelper.mdx", 0x58BD40A9 }, + TestHash{ "abcdefghijklmnopqrstuvwxyz", 0xEF065306 }, + TestHash{ "ABCDEFGHIJKLMNOPQRSTUVWXYZ", 0x29564240 }, + TestHash{ "123456789~!@#$%^&*()_+`-=[]{}|;':\",.<>?", 0x54E31141 }, + TestHash{ "/", 0x93912757 }, + TestHash{ "\\", 0x57EC56C7 }, + TestHash{ "\r\n\t ", 0x89351E43 } + ); + + auto hash = SStrHash(testcase.str, SSTR_HASH_CASESENSITIVE); + CHECK(hash == testcase.hash); + } + + SECTION("hashes strings with fixed seeds case insensitive") { + // Results obtained by directly calling Starcraft 1.17's SStrHash + auto testcase = GENERATE( + TestHash{ "bloop bloop", 0xDD85B006 }, + TestHash{ "BLOOP bloop", 0xDD85B006 }, + TestHash{ "Objects\\CameraHelper\\CameraHelper.mdx", 0x0EA7F863 }, + TestHash{ "Objects/CameraHelper/CameraHelper.mdx", 0x0EA7F863 }, + TestHash{ "abcdefghijklmnopqrstuvwxyz", 0x4BAB46BC }, + TestHash{ "ABCDEFGHIJKLMNOPQRSTUVWXYZ", 0x4BAB46BC }, + TestHash{ "123456789~!@#$%^&*()_+`-=[]{}|;':\",.<>?", 0x2A52D7D3 }, + TestHash{ "/", 0xD7DED775 }, + TestHash{ "\\", 0xD7DED775 }, + TestHash{ "\r\n\t ", 0xC0038449 } + ); + + auto hash = SStrHash(testcase.str, 0, 123); + CHECK(hash == testcase.hash); + } + + SECTION("hashes strings with fixed seeds case sensitive") { + // Results obtained by directly calling Starcraft 1.17's SStrHash + auto testcase = GENERATE( + TestHash{ "bloop bloop", 0x96187662 }, + TestHash{ "BLOOP bloop", 0xC421E888 }, + TestHash{ "Objects\\CameraHelper\\CameraHelper.mdx", 0x625F6763 }, + TestHash{ "Objects/CameraHelper/CameraHelper.mdx", 0xC2A7DFE7 }, + TestHash{ "abcdefghijklmnopqrstuvwxyz", 0x4018029E }, + TestHash{ "ABCDEFGHIJKLMNOPQRSTUVWXYZ", 0x4BAB46BC }, + TestHash{ "123456789~!@#$%^&*()_+`-=[]{}|;':\",.<>?", 0x2A52D7D3 }, + TestHash{ "/", 0x13A3A6E5 }, + TestHash{ "\\", 0xD7DED775 }, + TestHash{ "\r\n\t ", 0xC0038449 } + ); + + auto hash = SStrHash(testcase.str, SSTR_HASH_CASESENSITIVE, 123); + CHECK(hash == testcase.hash); + } + + SECTION("hashing empty string with seed of 0 gives default seed") { + auto hash = SStrHash("", 0, 0); + CHECK(hash == 0x7FED7FED); + + hash = SStrHash("", SSTR_HASH_CASESENSITIVE, 0); + CHECK(hash == 0x7FED7FED); + } + + SECTION("hashing empty string with custom seed returns that seed") { + auto seed = GENERATE(1, 123, 0x7FED7FED, 0xFFFFFFFF); + + auto hash = SStrHash("", 0, seed); + CHECK(hash == seed); + + hash = SStrHash("", SSTR_HASH_CASESENSITIVE, seed); + CHECK(hash == seed); + } +} + TEST_CASE("SStrHashHT", "[string]") { SECTION("hashes simple string correctly") { auto hash = SStrHashHT("foo");