diff options
-rw-r--r-- | src/BlockState.cpp | 167 | ||||
-rw-r--r-- | src/BlockState.h | 90 | ||||
-rw-r--r-- | src/CMakeLists.txt | 2 | ||||
-rw-r--r-- | tests/BlockTypeRegistry/BlockStateTest.cpp | 145 | ||||
-rw-r--r-- | tests/BlockTypeRegistry/CMakeLists.txt | 13 |
5 files changed, 417 insertions, 0 deletions
diff --git a/src/BlockState.cpp b/src/BlockState.cpp new file mode 100644 index 000000000..4e2607bd3 --- /dev/null +++ b/src/BlockState.cpp @@ -0,0 +1,167 @@ +#include "Globals.h" +#include "BlockState.h" + + + + + +BlockState::BlockState(): + mChecksum(initializeChecksum()) +{ + // Nothing needed yet +} + + + + + +BlockState::BlockState(const AString & aKey, const AString & aValue): + mState({{aKey, aValue}}), + mChecksum(initializeChecksum()) +{ +} + + + + + +BlockState::BlockState(std::initializer_list<std::pair<const AString, AString>> aKeysAndValues): + mState(aKeysAndValues), + mChecksum(initializeChecksum()) +{ +} + + + + + +BlockState::BlockState(const std::map<AString, AString> & aKeysAndValues): + mState(aKeysAndValues), + mChecksum(initializeChecksum()) +{ +} + + + + + +BlockState::BlockState(std::map<AString, AString> && aKeysAndValues): + mState(std::move(aKeysAndValues)), + mChecksum(initializeChecksum()) +{ +} + + + + + +BlockState::BlockState(const BlockState & aCopyFrom, std::initializer_list<std::pair<const AString, AString>> aAdditionalKeysAndValues): + mState(aCopyFrom.mState) +{ + for (const auto & kav: aAdditionalKeysAndValues) + { + mState[kav.first] = kav.second; + } + mChecksum = initializeChecksum(); +} + + + + + +BlockState::BlockState(const BlockState & aCopyFrom, const std::map<AString, AString> & aAdditionalKeysAndValues): + mState(aCopyFrom.mState) +{ + for (const auto & kav: aAdditionalKeysAndValues) + { + mState[kav.first] = kav.second; + } + mChecksum = initializeChecksum(); +} + + + + + +bool BlockState::operator ==(const BlockState & aOther) const +{ + // Fast-fail if the checksums differ or differrent counts: + if ((mChecksum != aOther.mChecksum) || (mState.size() != aOther.mState.size())) + { + return false; + } + + // Slow-check everything if the checksums match: + return std::equal(mState.begin(), mState.end(), aOther.mState.begin()); +} + + + + + +const AString & BlockState::value(const AString & aKey) const +{ + auto itr = mState.find(aKey); + if (itr == mState.end()) + { + static AString empty; + return empty; + } + return itr->second; +} + + + + + +UInt32 BlockState::initializeChecksum() +{ + removeEmptyKeys(); + + // Calculate the checksum as a XOR of all mState keys' and values' checksums + // This way we don't depend on the std::map's ordering + UInt32 res = 0; + for (const auto & kv: mState) + { + auto partial = partialChecksum(kv.first) ^ partialChecksum(kv.second); + res = res ^ partial; + } + return res; +} + + + + + +void BlockState::removeEmptyKeys() +{ + for (auto itr = mState.begin(); itr != mState.end();) + { + if (itr->second.empty()) + { + itr = mState.erase(itr); + } + else + { + ++itr; + } + } +} + + + + + +UInt32 BlockState::partialChecksum(const AString & aString) +{ + UInt32 shift = 0; + UInt32 res = 0; + for (auto ch: aString) + { + UInt32 v = static_cast<UInt8>(ch); + v = v << shift; + shift = (shift + 1) % 24; + res = res ^ v; + } + return res; +} diff --git a/src/BlockState.h b/src/BlockState.h new file mode 100644 index 000000000..3b0575f0e --- /dev/null +++ b/src/BlockState.h @@ -0,0 +1,90 @@ +#pragma once + +#include <initializer_list> + + + + + +/** Represents the state of a single block (previously known as "block meta"). +The state consists of a map of string -> string, plus a mechanism for fast equality checks between two BlockState instances. +Once a BlockState instance is created, it is then immutable - there's no way of changing it, only by creating a (modified) copy. +A BlockState instance can be created from hard-coded data or from dynamic data: + BlockState bs({{"key1", "value1"}, {key2", "value2"}}); // Hard-coded + - or - + std::map<AString, AString> map({{"key1", "value1"}, {key2", "value2"}}); + map["key3"] = "value3"; + BlockState bs(map); // From dynamic data +*/ +class BlockState +{ +public: + + /** Creates a new instance with an empty map. */ + BlockState(); + + /** Creates a new instance consisting of a single key-value pair. + If the value is empty, it is not stored wihin the map. */ + BlockState(const AString & aKey, const AString & aValue); + + /** Creates a new instance initialized with several (hard-coded) key-value pairs. + Any key with an empty value is not stored within the map. */ + BlockState(std::initializer_list<std::pair<const AString, AString>> aKeysAndValues); + + /** Creates a new instance initialized with several (dynamic) key-value pairs. + Makes a copy of aKeysAndValues for this object. + Any key with an empty value is not stored within the map. */ + BlockState(const std::map<AString, AString> & aKeysAndValues); + + /** Creates a new instance initialized with several (dynamic) key-value pairs. + Any key with an empty value is not stored within the map. */ + BlockState(std::map<AString, AString> && aKeysAndValues); + + /** Creates a copy of the specified BlockState with the (hard-coded) additional keys and values added to it. + Any key in aAdditionalKeysAndValues that is already present in aCopyFrom is overwritten with the aAdditionalKeysAndValues' one. + Any key with an empty value is not stored in the map. + (it's possible to erase a key from aCopyFrom by setting it to empty string in aAdditionalKeysAndValues). */ + BlockState(const BlockState & aCopyFrom, std::initializer_list<std::pair<const AString, AString>> aAdditionalKeysAndValues); + + /** Creates a copy of the specified BlockState with the (dynamic) additional keys and values added to it. + Any key in aAdditionalKeysAndValues that is already present in aCopyFrom is overwritten with the aAdditionalKeysAndValues' one. + Any key with an empty value is not stored in the map. + (it's possible to erase a key from aCopyFrom by setting it to empty string in aAdditionalKeysAndValues). */ + BlockState(const BlockState & aCopyFrom, const std::map<AString, AString> & aAdditionalKeysAndValues); + + /** Fast equality check. */ + bool operator ==(const BlockState & aOther) const; + + /** Fast inequality check. */ + bool operator !=(const BlockState & aOther) const + { + return !(operator ==(aOther)); + } + + /** Returns the value at the specified key. + If the key is not present, returns an empty string. */ + const AString & value(const AString & aKey) const; + + +protected: + + /** The state, represented as a string->string map. */ + std::map<AString, AString> mState; + + /** The checksum used for the fast equality check. + This is calculated upon creation. */ + UInt32 mChecksum; + + + /** Normalizes mState and calculates the checksum from it. + Removes all the empty values from mState. + Used only from constructors. */ + UInt32 initializeChecksum(); + + /** Removes all the keys from mState that have an empty value. */ + void removeEmptyKeys(); + + /** Calculates the partial checksum of a single string. + Used from within initializeChecksum(). */ + UInt32 partialChecksum(const AString & aString); +}; diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index d23fe388d..dd92a969c 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -17,6 +17,7 @@ SET (SRCS BlockArea.cpp BlockID.cpp BlockInfo.cpp + BlockState.cpp BlockTypeRegistry.cpp BrewingRecipes.cpp Broadcaster.cpp @@ -84,6 +85,7 @@ SET (HDRS BlockID.h BlockInServerPluginInterface.h BlockInfo.h + BlockState.h BlockTracer.h BlockTypeRegistry.h BrewingRecipes.h diff --git a/tests/BlockTypeRegistry/BlockStateTest.cpp b/tests/BlockTypeRegistry/BlockStateTest.cpp new file mode 100644 index 000000000..d3ce92455 --- /dev/null +++ b/tests/BlockTypeRegistry/BlockStateTest.cpp @@ -0,0 +1,145 @@ +#include "Globals.h" +#include "BlockState.h" +#include "../TestHelpers.h" + + + + +/** Tests the class constructors with static (hard-coded) data. */ +static void testStaticCreation() +{ + LOGD("Testing BlockState creation from static data..."); + + // Create a few BlockStates using the static-data constructors: + BlockState bs1v1; + BlockState bs2v1("property", "value"); + BlockState bs3v1({{"property1", "value1"}, {"property2", "value2"}}); + BlockState bs1v2(bs1v1); + BlockState bs2v2(bs2v1); + BlockState bs3v2(bs3v1); + BlockState bs1v3(bs1v2, {{"added property", "value1"}}); + BlockState bs2v3(bs2v2, {{"added property", "value2"}}); + BlockState bs3v3(bs3v2, {{"added property", "value3"}}); + + // Test (in-)equality (v1 = v2 != v3): + TEST_EQUAL(bs1v1, bs1v2); + TEST_EQUAL(bs2v1, bs2v2); + TEST_EQUAL(bs3v1, bs3v2); + TEST_NOTEQUAL(bs1v1, bs1v3); + TEST_NOTEQUAL(bs2v1, bs2v3); + TEST_NOTEQUAL(bs3v1, bs3v3); + TEST_NOTEQUAL(bs1v2, bs1v3); + TEST_NOTEQUAL(bs2v2, bs2v3); + TEST_NOTEQUAL(bs3v2, bs3v3); + + // Test that the values are actually stored: + TEST_EQUAL(bs1v1.value("property"), ""); + TEST_EQUAL(bs2v1.value("property"), "value"); + TEST_EQUAL(bs2v1.value("property1"), ""); + TEST_EQUAL(bs3v1.value("property1"), "value1"); + TEST_EQUAL(bs3v1.value("property2"), "value2"); + TEST_EQUAL(bs1v3.value("added property"), "value1"); + TEST_EQUAL(bs2v3.value("added property"), "value2"); + TEST_EQUAL(bs3v3.value("added property"), "value3"); +} + + + + + +/** Tests the dynamic-data constructors (map param, deep-copy). */ +static void testDynamicCreation() +{ + LOGD("Testing BlockState creation from dynamic data..."); + + using Map = std::map<AString, AString>; + + // Construct from scratch: + { + BlockState bs1a({{"property", "value"}}); + Map map1({{"property", "value"}}); + BlockState bs1b(map1); + TEST_EQUAL(bs1a, bs1b); // Creation works + map1.clear(); + TEST_EQUAL(bs1a, bs1b); // Created a copy independent of map1 + } + + // Construct by moving: + { + BlockState bs2a({{"property", "value"}}); + Map map2({{"property", "value"}}); + BlockState bs2b(std::move(map2)); + TEST_EQUAL(bs2a, bs2b); // Creation works + } + + // Construct by modifying: + { + BlockState bsSrc("property1", "value1"); + BlockState bs3a(bsSrc, {{"property2", "value2"}}); + Map map3({{"property2", "value2"}}); + BlockState bs3b(bsSrc, map3); + TEST_EQUAL(bs3a, bs3b); + map3.clear(); + TEST_EQUAL(bs3a, bs3b); + } +} + + + + + +/** Tests replacing the properties in the copy-and-modify constructors. */ +static void testReplacing() +{ + LOGD("Testing replacing / removing properties in BlockState copies..."); + + // Test replacing: + BlockState bs1("property1", "value1v1"); + BlockState bs2(bs1, {{"property1", "value1v2"}}); + TEST_EQUAL(bs2.value("property1"), "value1v2"); // Stored the new one + TEST_EQUAL(bs1.value("property1"), "value1v1"); // Didn't replace in the original + + // Test removing: + BlockState bs3(bs1, {{"property1", ""}}); + BlockState bsEmpty; + TEST_EQUAL(bs3, bsEmpty); +} + + + + + +int main() +{ + LOGD("BlockStateTest started"); + + try + { + testStaticCreation(); + testDynamicCreation(); + testReplacing(); + } + catch (const TestException & exc) + { + LOGERROR("BlockStateTest has failed explicitly: %s", exc.mMessage.c_str()); + return 1; + } + catch (const std::runtime_error & exc) + { + LOGERROR("BlockStateTest has failed, an unhandled exception was thrown: %s", exc.what()); + return 1; + } + catch (...) + { + LOGERROR("BlockStateTest has failed, an unknown exception was thrown."); + return 1; + } + + LOGD("BlockStateTest finished"); + + return 0; +} + + + + diff --git a/tests/BlockTypeRegistry/CMakeLists.txt b/tests/BlockTypeRegistry/CMakeLists.txt index 25b18c373..c95c503dd 100644 --- a/tests/BlockTypeRegistry/CMakeLists.txt +++ b/tests/BlockTypeRegistry/CMakeLists.txt @@ -11,6 +11,16 @@ add_definitions(-DTEST_GLOBALS=1) # Define individual test executables: +# BlockStateTest: Verify that the BlockState class works as intended: +add_executable(BlockStateTest + BlockStateTest.cpp + ../TestHelpers.h + ${CMAKE_SOURCE_DIR}/src/BlockState.cpp + ${CMAKE_SOURCE_DIR}/src/StringUtils.cpp + ${CMAKE_SOURCE_DIR}/src/OSSupport/CriticalSection.cpp +) +target_link_libraries(BlockStateTest fmt::fmt) + # BlockTypeRegistryTest: Verify that the BlockTypeRegistry class works as intended: add_executable(BlockTypeRegistryTest BlockTypeRegistryTest.cpp @@ -25,8 +35,10 @@ target_link_libraries(BlockTypeRegistryTest fmt::fmt) + # Define individual tests: +add_test(NAME BlockStateTest COMMAND BlockStateTest) add_test(NAME BlockTypeRegistryTest COMMAND BlockTypeRegistryTest) @@ -35,6 +47,7 @@ add_test(NAME BlockTypeRegistryTest COMMAND BlockTypeRegistryTest) # Put all the tests into a solution folder (MSVC): set_target_properties( + BlockStateTest BlockTypeRegistryTest PROPERTIES FOLDER Tests/BlockTypeRegistry ) |