From f7e0cb3c455c4dbcaca75f7396a97c460c65a874 Mon Sep 17 00:00:00 2001 From: Jan Laupetin Date: Sat, 21 Feb 2026 10:52:36 +0000 Subject: [PATCH] chore: call common djb2 implementations in game hashing funcs --- src/Common/Game/IW3/CommonIW3.h | 18 ++-- src/Common/Game/IW4/CommonIW4.cpp | 18 ---- src/Common/Game/IW4/CommonIW4.h | 32 ++++--- src/Common/Game/IW5/CommonIW5.cpp | 18 ---- src/Common/Game/IW5/CommonIW5.h | 28 ++++-- src/Common/Game/T5/CommonT5.cpp | 54 ----------- src/Common/Game/T5/CommonT5.h | 36 ++++++-- src/Common/Game/T6/CommonT6.h | 45 ++------- src/Common/Utils/Djb2.h | 100 ++++++++++++++++++++ test/CommonTests/Utils/Djb2Tests.cpp | 133 +++++++++++++++++++++++++++ 10 files changed, 318 insertions(+), 164 deletions(-) create mode 100644 src/Common/Utils/Djb2.h create mode 100644 test/CommonTests/Utils/Djb2Tests.cpp diff --git a/src/Common/Game/IW3/CommonIW3.h b/src/Common/Game/IW3/CommonIW3.h index 43664b62..68097801 100644 --- a/src/Common/Game/IW3/CommonIW3.h +++ b/src/Common/Game/IW3/CommonIW3.h @@ -1,28 +1,24 @@ #pragma once #include "IW3.h" +#include "Utils/Djb2.h" + +#include namespace IW3 { class Common { public: - static constexpr uint32_t R_HashString(const char* string, const uint32_t hash) + static constexpr uint32_t R_HashString(const char* str, const uint32_t hash) { - const char* v2 = string; // edx@1 - char v3 = *string; // cl@1 - uint32_t result = hash; - - for (; *v2; v3 = *v2) - { - ++v2; - result = 33 * result ^ (v3 | 0x20); - } - return result; + return djb2_xor_nocase(str, hash); } static constexpr uint32_t R_HashString(const char* string) { + // Using djb2 with a 0 starting value makes a worse hash func apparently + // but who am I to judge return R_HashString(string, 0u); } diff --git a/src/Common/Game/IW4/CommonIW4.cpp b/src/Common/Game/IW4/CommonIW4.cpp index 6b3b6f97..7fdc4bc3 100644 --- a/src/Common/Game/IW4/CommonIW4.cpp +++ b/src/Common/Game/IW4/CommonIW4.cpp @@ -2,26 +2,8 @@ #include "Utils/Pack.h" -#include - using namespace IW4; -int Common::StringTable_HashString(const char* str) -{ - if (!str) - return 0; - - auto result = 0; - auto offset = 0; - while (str[offset]) - { - const auto c = tolower(str[offset++]); - result = c + 31 * result; - } - - return result; -} - PackedTexCoords Common::Vec2PackTexCoords(const float (&in)[2]) { return PackedTexCoords{pack32::Vec2PackTexCoordsVU(in)}; diff --git a/src/Common/Game/IW4/CommonIW4.h b/src/Common/Game/IW4/CommonIW4.h index 9aaff02c..7d19edbf 100644 --- a/src/Common/Game/IW4/CommonIW4.h +++ b/src/Common/Game/IW4/CommonIW4.h @@ -1,33 +1,41 @@ #pragma once #include "IW4.h" +#include "Utils/Djb2.h" + +#include namespace IW4 { class Common { public: - static constexpr uint32_t R_HashString(const char* string, const uint32_t hash) + static constexpr int StringTable_HashString(const char* str) { - const char* v2 = string; // edx@1 - char v3 = *string; // cl@1 - uint32_t result = hash; + if (!str) + return 0; - for (; *v2; v3 = *v2) - { - ++v2; - result = 33 * result ^ (v3 | 0x20); - } - return result; + // Lets do djb2 with 31 instead of 33 because why not + // and leave out the starting value while we are at it + uint32_t hash = 0; + for (char c = *str; c; c = *++str) + hash = hash * 31 + std::tolower(c); + + return static_cast(hash); + } + + static constexpr uint32_t R_HashString(const char* str, const uint32_t hash) + { + return djb2_xor_nocase(str, hash); } static constexpr uint32_t R_HashString(const char* string) { + // Using djb2 with a 0 starting value makes a worse hash func apparently + // but who am I to judge return R_HashString(string, 0u); } - static int StringTable_HashString(const char* str); - static PackedTexCoords Vec2PackTexCoords(const float (&in)[2]); static PackedUnitVec Vec3PackUnitVec(const float (&in)[3]); static GfxColor Vec4PackGfxColor(const float (&in)[4]); diff --git a/src/Common/Game/IW5/CommonIW5.cpp b/src/Common/Game/IW5/CommonIW5.cpp index 55b820fc..98d61d2f 100644 --- a/src/Common/Game/IW5/CommonIW5.cpp +++ b/src/Common/Game/IW5/CommonIW5.cpp @@ -2,26 +2,8 @@ #include "Utils/Pack.h" -#include - using namespace IW5; -int Common::StringTable_HashString(const char* str) -{ - if (!str) - return 0; - - auto result = 0; - auto offset = 0; - while (str[offset]) - { - const auto c = tolower(str[offset++]); - result = c + 31 * result; - } - - return result; -} - PackedTexCoords Common::Vec2PackTexCoords(const float (&in)[2]) { return PackedTexCoords{pack32::Vec2PackTexCoordsVU(in)}; diff --git a/src/Common/Game/IW5/CommonIW5.h b/src/Common/Game/IW5/CommonIW5.h index 33fac614..5da87f86 100644 --- a/src/Common/Game/IW5/CommonIW5.h +++ b/src/Common/Game/IW5/CommonIW5.h @@ -1,26 +1,38 @@ #pragma once #include "IW5.h" +#include "Utils/Djb2.h" + +#include namespace IW5 { class Common { public: - static int StringTable_HashString(const char* str); - - static constexpr uint32_t R_HashString(const char* str, uint32_t hash) + static constexpr int StringTable_HashString(const char* str) { - for (const auto* pos = str; *pos; pos++) - { - hash = 33 * hash ^ (*pos | 0x20); - } + if (!str) + return 0; - return hash; + // Lets do djb2 with 31 instead of 33 because why not + // and leave out the starting value while we are at it + uint32_t hash = 0; + for (char c = *str; c; c = *++str) + hash = hash * 31 + std::tolower(c); + + return static_cast(hash); + } + + static constexpr uint32_t R_HashString(const char* str, const uint32_t hash) + { + return djb2_xor_nocase(str, hash); } static constexpr uint32_t R_HashString(const char* string) { + // Using djb2 with a 0 starting value makes a worse hash func apparently + // but who am I to judge return R_HashString(string, 0u); } diff --git a/src/Common/Game/T5/CommonT5.cpp b/src/Common/Game/T5/CommonT5.cpp index 979f0300..c09ce3da 100644 --- a/src/Common/Game/T5/CommonT5.cpp +++ b/src/Common/Game/T5/CommonT5.cpp @@ -2,62 +2,8 @@ #include "Utils/Pack.h" -#include - using namespace T5; -int Common::Com_HashKey(const char* str, const int maxLen) -{ - if (str == nullptr) - return 0; - - int hash = 0; - for (int i = 0; i < maxLen; i++) - { - if (str[i] == '\0') - break; - - hash += str[i] * (0x77 + i); - } - - return hash ^ ((hash ^ (hash >> 10)) >> 10); -} - -int Common::Com_HashString(const char* str) -{ - if (!str) - return 0; - - auto result = 0x1505; - auto offset = 0; - while (str[offset]) - { - const auto c = tolower(str[offset++]); - result = c + 33 * result; - } - - return result; -} - -int Common::Com_HashString(const char* str, const int len) -{ - if (!str) - return 0; - - int result = 0x1505; - int offset = 0; - while (str[offset]) - { - if (len > 0 && offset >= len) - break; - - const int c = tolower(str[offset++]); - result = c + 33 * result; - } - - return result; -} - PackedTexCoords Common::Vec2PackTexCoords(const float (&in)[2]) { return PackedTexCoords{pack32::Vec2PackTexCoordsVU(in)}; diff --git a/src/Common/Game/T5/CommonT5.h b/src/Common/Game/T5/CommonT5.h index c5e4d762..c743ab96 100644 --- a/src/Common/Game/T5/CommonT5.h +++ b/src/Common/Game/T5/CommonT5.h @@ -1,28 +1,48 @@ #pragma once #include "T5.h" +#include "Utils/Djb2.h" namespace T5 { class Common { public: - static int Com_HashKey(const char* str, int maxLen); - static int Com_HashString(const char* str); - static int Com_HashString(const char* str, int len); - - static constexpr uint32_t R_HashString(const char* str, uint32_t hash) + static constexpr int Com_HashKey(const char* str, const int maxLen) { - for (const auto* pos = str; *pos; pos++) + if (str == nullptr) + return 0; + + int hash = 0; + for (int i = 0; i < maxLen; i++) { - hash = 33 * hash ^ (*pos | 0x20); + if (str[i] == '\0') + break; + + hash += str[i] * (0x77 + i); } - return hash; + return hash ^ ((hash ^ (hash >> 10)) >> 10); + } + + static constexpr int Com_HashString(const char* str) + { + // Hashing aesthetics seem to be a thing + if (!str) + return 0; + + return static_cast(djb2_lower(str)); + } + + static constexpr uint32_t R_HashString(const char* str, const uint32_t hash) + { + return djb2_xor_nocase(str, hash); } static constexpr uint32_t R_HashString(const char* string) { + // Using djb2 with a 0 starting value makes a worse hash func apparently + // but who am I to judge return R_HashString(string, 0u); } diff --git a/src/Common/Game/T6/CommonT6.h b/src/Common/Game/T6/CommonT6.h index 212a09d9..a04f657b 100644 --- a/src/Common/Game/T6/CommonT6.h +++ b/src/Common/Game/T6/CommonT6.h @@ -1,6 +1,7 @@ #pragma once #include "T6.h" +#include "Utils/Djb2.h" #include @@ -28,51 +29,22 @@ namespace T6 static constexpr int Com_HashString(const char* str) { + // Hashing aesthetics seem to be a thing if (!str) return 0; - auto result = 0x1505; - auto offset = 0; - while (str[offset]) - { - const auto c = tolower(str[offset++]); - result = c + 33 * result; - } - - return result; + return static_cast(djb2_lower(str)); } - static constexpr int Com_HashString(const char* str, const int len) + static constexpr uint32_t R_HashString(const char* str, const uint32_t hash) { - if (!str) - return 0; - - int result = 0x1505; - int offset = 0; - while (str[offset]) - { - if (len > 0 && offset >= len) - break; - - const int c = tolower(str[offset++]); - result = c + 33 * result; - } - - return result; - } - - static constexpr uint32_t R_HashString(const char* str, uint32_t hash) - { - for (const auto* pos = str; *pos; pos++) - { - hash = 33 * hash ^ (*pos | 0x20); - } - - return hash; + return djb2_xor_nocase(str, hash); } static constexpr uint32_t R_HashString(const char* string) { + // Using djb2 with a 0 starting value makes a worse hash func apparently + // but who am I to judge return R_HashString(string, 0u); } @@ -81,6 +53,9 @@ namespace T6 if (!str || !*str) return 0; + // Seems to be somewhat based on sdbm + // http://www.cse.yorku.ca/~oz/hash.html + auto result = 0x1505; auto offset = 0u; diff --git a/src/Common/Utils/Djb2.h b/src/Common/Utils/Djb2.h new file mode 100644 index 00000000..163fe2a6 --- /dev/null +++ b/src/Common/Utils/Djb2.h @@ -0,0 +1,100 @@ +#pragma once + +#include +#include + +// This header contains multiple varying implementations of the DJB2 algorithm: +// http://www.cse.yorku.ca/~oz/hash.html + +constexpr uint32_t DJB2_STARTING_VALUE = 5381; + +static constexpr uint32_t djb2(const char* str, uint32_t hash) +{ + for (char c = *str; c; c = *++str) + { + // hash * 33 + c + hash = ((hash << 5) + hash) + c; + } + + return hash; +} + +static constexpr uint32_t djb2(const char* str) +{ + return djb2(str, DJB2_STARTING_VALUE); +} + +static constexpr uint32_t djb2_nocase(const char* str, uint32_t hash) +{ + for (char c = *str; c; c = *++str) + { + // Or with 0x20 makes the string case-insensitive + // but also messes up non-letters + hash = ((hash << 5) + hash) + (c | 0x20); + } + + return hash; +} + +static constexpr uint32_t djb2_nocase(const char* str) +{ + return djb2_nocase(str, DJB2_STARTING_VALUE); +} + +static constexpr uint32_t djb2_lower(const char* str, uint32_t hash) +{ + for (char c = *str; c; c = *++str) + hash = ((hash << 5) + hash) + std::tolower(c); + + return hash; +} + +static constexpr uint32_t djb2_lower(const char* str) +{ + return djb2_lower(str, DJB2_STARTING_VALUE); +} + +static constexpr uint32_t djb2_xor(const char* str, uint32_t hash) +{ + for (char c = *str; c; c = *++str) + { + hash = ((hash << 5) + hash) ^ c; + } + + return hash; +} + +static constexpr uint32_t djb2_xor(const char* str) +{ + return djb2_xor(str, DJB2_STARTING_VALUE); +} + +static constexpr uint32_t djb2_xor_nocase(const char* str, uint32_t hash) +{ + for (char c = *str; c; c = *++str) + { + // Or with 0x20 makes the string case-insensitive + // but also messes up non-letters + hash = ((hash << 5) + hash) ^ (c | 0x20); + } + + return hash; +} + +static constexpr uint32_t djb2_xor_nocase(const char* str) +{ + return djb2_xor_nocase(str, DJB2_STARTING_VALUE); +} + +static constexpr uint32_t djb2_xor_lower(const char* str, uint32_t hash) +{ + for (char c = *str; c; c = *++str) + hash = ((hash << 5) + hash) ^ std::tolower(c); + + return hash; +} + +static constexpr uint32_t djb2_xor_lower(const char* str) +{ + return djb2_xor_lower(str, DJB2_STARTING_VALUE); +} diff --git a/test/CommonTests/Utils/Djb2Tests.cpp b/test/CommonTests/Utils/Djb2Tests.cpp new file mode 100644 index 00000000..f7e0aa8c --- /dev/null +++ b/test/CommonTests/Utils/Djb2Tests.cpp @@ -0,0 +1,133 @@ +#include "Utils/Djb2.h" + +#include +#include + +TEST_CASE("Djb2: Check checksums", "[djb2]") +{ + SECTION("for djb2") + { + const auto [str, expectedHash] = GENERATE(Catch::Generators::table({ + {"hello world", 0x3551c8c1}, + {"universe2", 0x608d72a8}, + {"lit_r0c0n0x0_b1c1n1s1v1_b2c2n2x2", 0xf93c46aa}, + {"AngularVelocityScale", 0xbba68366}, + {"BakedLightingIntensity", 0x98a9e159}, + {"Layer1OffsetBobbleDelay", 0x319c46af}, + {"MaxDepth", 0xd34432a0}, + {"MomentumColor", 0x33a353d6}, + {"SparkleScale", 0x2249309f}, + {"TickMarkColorAndHarshness", 0x0d77a53c}, + {"worldViewProjectionMatrix", 0xcf668f9a}, + })); + + CAPTURE(str); + const auto hash = djb2(str); + REQUIRE(hash == expectedHash); + } + + SECTION("for djb2_nocase") + { + const auto [str, expectedHash] = GENERATE(Catch::Generators::table({ + {"hello world", 0x3551c8c1}, + {"universe2", 0x608d72a8}, + {"lit_r0c0n0x0_b1c1n1s1v1_b2c2n2x2", 0xd12da30a}, + {"AngularVelocityScale", 0x2eb08fc6}, + {"BakedLightingIntensity", 0xd8fd95b9}, + {"Layer1OffsetBobbleDelay", 0x4166172f}, + {"MaxDepth", 0x5dc1dee0}, + {"MomentumColor", 0x21c79416}, + {"SparkleScale", 0x42f7ecdf}, + {"TickMarkColorAndHarshness", 0xbb5761dc}, + {"worldViewProjectionMatrix", 0x25962bfa}, + })); + + CAPTURE(str); + const auto hash = djb2_nocase(str); + REQUIRE(hash == expectedHash); + } + + SECTION("for djb2_lower") + { + const auto [str, expectedHash] = GENERATE(Catch::Generators::table({ + {"hello world", 0x3551c8c1}, + {"universe2", 0x608d72a8}, + {"lit_r0c0n0x0_b1c1n1s1v1_b2c2n2x2", 0xf93c46aa}, + {"AngularVelocityScale", 0x2eb08fc6}, + {"BakedLightingIntensity", 0xd8fd95b9}, + {"Layer1OffsetBobbleDelay", 0x4166172f}, + {"MaxDepth", 0x5dc1dee0}, + {"MomentumColor", 0x21c79416}, + {"SparkleScale", 0x42f7ecdf}, + {"TickMarkColorAndHarshness", 0xbb5761dc}, + {"worldViewProjectionMatrix", 0x25962bfa}, + })); + + CAPTURE(str); + const auto hash = djb2_lower(str); + REQUIRE(hash == expectedHash); + } + + SECTION("for djb2_xor") + { + const auto [str, expectedHash] = GENERATE(Catch::Generators::table({ + {"hello world", 0xf8c65345}, + {"universe2", 0xdce33172}, + {"lit_r0c0n0x0_b1c1n1s1v1_b2c2n2x2", 0x7e4ed3e0}, + {"AngularVelocityScale", 0xf65bbb88}, + {"BakedLightingIntensity", 0xd79467bd}, + {"Layer1OffsetBobbleDelay", 0xf033afcb}, + {"MaxDepth", 0x190e08dc}, + {"MomentumColor", 0xf4ded370}, + {"SparkleScale", 0x9358a5ef}, + {"TickMarkColorAndHarshness", 0x12d2e5f8}, + {"worldViewProjectionMatrix", 0x88669e4c}, + })); + + CAPTURE(str); + const auto hash = djb2_xor(str); + REQUIRE(hash == expectedHash); + } + + SECTION("for djb2_xor_nocase") + { + const auto [str, expectedHash] = GENERATE(Catch::Generators::table({ + {"hello world", 0xf8c65345}, + {"universe2", 0xdce33172}, + {"lit_r0c0n0x0_b1c1n1s1v1_b2c2n2x2", 0x3cc06840}, + {"AngularVelocityScale", 0x7efc36a8}, + {"BakedLightingIntensity", 0x3d52c39d}, + {"Layer1OffsetBobbleDelay", 0x48574d8b}, + {"MaxDepth", 0x3c5b84dc}, + {"MomentumColor", 0x4d32f230}, + {"SparkleScale", 0x7656c02f}, + {"TickMarkColorAndHarshness", 0x917d8218}, + {"worldViewProjectionMatrix", 0x721c412c}, + })); + + CAPTURE(str); + const auto hash = djb2_xor_nocase(str); + REQUIRE(hash == expectedHash); + } + + SECTION("for djb2_xor_lower") + { + const auto [str, expectedHash] = GENERATE(Catch::Generators::table({ + {"hello world", 0xf8c65345}, + {"universe2", 0xdce33172}, + {"lit_r0c0n0x0_b1c1n1s1v1_b2c2n2x2", 0x7e4ed3e0}, + {"AngularVelocityScale", 0x7efc36a8}, + {"BakedLightingIntensity", 0x3d52c39d}, + {"Layer1OffsetBobbleDelay", 0x48574d8b}, + {"MaxDepth", 0x3c5b84dc}, + {"MomentumColor", 0x4d32f230}, + {"SparkleScale", 0x7656c02f}, + {"TickMarkColorAndHarshness", 0x917d8218}, + {"worldViewProjectionMatrix", 0x721c412c}, + })); + + CAPTURE(str); + const auto hash = djb2_xor_lower(str); + REQUIRE(hash == expectedHash); + } +}