summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--src/BlockTypeRegistry.cpp153
-rw-r--r--src/BlockTypeRegistry.h158
-rw-r--r--src/CMakeLists.txt2
-rw-r--r--tests/BlockTypeRegistry/BlockTypeRegistryTest.cpp228
-rw-r--r--tests/BlockTypeRegistry/CMakeLists.txt40
-rw-r--r--tests/CMakeLists.txt1
-rw-r--r--tests/TestHelpers.h79
7 files changed, 661 insertions, 0 deletions
diff --git a/src/BlockTypeRegistry.cpp b/src/BlockTypeRegistry.cpp
new file mode 100644
index 000000000..45ad20082
--- /dev/null
+++ b/src/BlockTypeRegistry.cpp
@@ -0,0 +1,153 @@
+
+#include "Globals.h"
+#include "BlockTypeRegistry.h"
+
+
+
+
+////////////////////////////////////////////////////////////////////////////////
+// BlockInfo:
+
+BlockInfo::BlockInfo(
+ const AString & aPluginName,
+ const AString & aBlockTypeName,
+ std::shared_ptr<cBlockHandler> aHandler,
+ const std::map<AString, AString> & aHints,
+ const std::map<AString, BlockInfo::HintCallback> & aHintCallbacks
+):
+ mPluginName(aPluginName),
+ mBlockTypeName(aBlockTypeName),
+ mHandler(aHandler),
+ mHints(aHints),
+ mHintCallbacks(aHintCallbacks)
+{
+}
+
+
+
+
+
+AString BlockInfo::hintValue(
+ const AString & aHintName,
+ const BlockState & aBlockState
+)
+{
+ // Search the hint callbacks first:
+ auto itrC = mHintCallbacks.find(aHintName);
+ if (itrC != mHintCallbacks.end())
+ {
+ // Hint callback found, use it:
+ return itrC->second(mBlockTypeName, aBlockState);
+ }
+
+ // Search the static hints:
+ auto itr = mHints.find(aHintName);
+ if (itr != mHints.end())
+ {
+ // Hint found, use it:
+ return itr->second;
+ }
+
+ // Nothing found, return empty string:
+ return AString();
+}
+
+
+
+
+
+////////////////////////////////////////////////////////////////////////////////
+// BlockTypeRegistry:
+
+void BlockTypeRegistry::registerBlockType(
+ const AString & aPluginName,
+ const AString & aBlockTypeName,
+ std::shared_ptr<cBlockHandler> aHandler,
+ const std::map<AString, AString> & aHints,
+ const std::map<AString, BlockInfo::HintCallback> & aHintCallbacks
+)
+{
+ auto blockInfo = std::make_shared<BlockInfo>(aPluginName, aBlockTypeName, aHandler, aHints, aHintCallbacks);
+
+ // Check previous registrations:
+ cCSLock lock(mCSRegistry);
+ auto itr = mRegistry.find(aBlockTypeName);
+ if (itr != mRegistry.end())
+ {
+ if (itr->second->pluginName() != aPluginName)
+ {
+ throw AlreadyRegisteredException(itr->second, blockInfo);
+ }
+ }
+
+ // Store the registration:
+ mRegistry[aBlockTypeName] = blockInfo;
+}
+
+
+
+
+
+std::shared_ptr<BlockInfo> BlockTypeRegistry::blockInfo(const AString & aBlockTypeName)
+{
+ cCSLock lock(mCSRegistry);
+ auto itr = mRegistry.find(aBlockTypeName);
+ if (itr == mRegistry.end())
+ {
+ return nullptr;
+ }
+ return itr->second;
+}
+
+
+
+
+
+void BlockTypeRegistry::removeAllByPlugin(const AString & aPluginName)
+{
+ cCSLock lock(mCSRegistry);
+ for (auto itr = mRegistry.begin(); itr != mRegistry.end();)
+ {
+ if (itr->second->pluginName() == aPluginName)
+ {
+ itr = mRegistry.erase(itr);
+ }
+ else
+ {
+ ++itr;
+ }
+ }
+}
+
+
+
+
+
+////////////////////////////////////////////////////////////////////////////////
+// BlockTypeRegistry::AlreadyRegisteredException:
+
+BlockTypeRegistry::AlreadyRegisteredException::AlreadyRegisteredException(
+ std::shared_ptr<BlockInfo> aPreviousRegistration,
+ std::shared_ptr<BlockInfo> aNewRegistration
+) :
+ Super(message(aPreviousRegistration, aNewRegistration)),
+ mPreviousRegistration(aPreviousRegistration),
+ mNewRegistration(aNewRegistration)
+{
+}
+
+
+
+
+
+AString BlockTypeRegistry::AlreadyRegisteredException::message(
+ std::shared_ptr<BlockInfo> aPreviousRegistration,
+ std::shared_ptr<BlockInfo> aNewRegistration
+)
+{
+ return Printf("Attempting to register BlockTypeName %s from plugin %s, while it is already registered in plugin %s",
+ aNewRegistration->blockTypeName().c_str(),
+ aNewRegistration->pluginName().c_str(),
+ aPreviousRegistration->pluginName().c_str()
+ );
+}
diff --git a/src/BlockTypeRegistry.h b/src/BlockTypeRegistry.h
new file mode 100644
index 000000000..6a24445c5
--- /dev/null
+++ b/src/BlockTypeRegistry.h
@@ -0,0 +1,158 @@
+#pragma once
+
+
+
+
+
+#include <map>
+
+
+
+
+
+// fwd:
+class BlockState;
+
+
+
+
+
+/** Complete information about a single block type.
+The BlockTypeRegistry uses this structure to store the registered information. */
+class BlockInfo
+{
+public:
+
+ /** Callback is used to query block hints dynamically, based on the current BlockState.
+ Useful for example for redstone lamps that can be turned on or off. */
+ using HintCallback = std::function<AString(const AString & aTypeName, const BlockState & aBlockState)>;
+
+
+ /** Creates a new instance with the specified BlockTypeName and handler / hints / callbacks.
+ aPluginName specifies the name of the plugin to associate with the block type (to allow unload / reload). */
+ BlockInfo(
+ const AString & aPluginName,
+ const AString & aBlockTypeName,
+ std::shared_ptr<cBlockHandler> aHandler,
+ const std::map<AString, AString> & aHints = std::map<AString, AString>(),
+ const std::map<AString, HintCallback> & aHintCallbacks = std::map<AString, HintCallback>()
+ );
+
+
+ /** Retrieves the value associated with the specified hint for this specific BlockTypeName and BlockState.
+ Queries callbacks first, then hints if a callback doesn't exist.
+ Returns an empty string if hint not found at all. */
+ AString hintValue(
+ const AString & aHintName,
+ const BlockState & aBlockState
+ );
+
+ // Simple getters:
+ const AString & pluginName() const { return mPluginName; }
+ const AString & blockTypeName() const { return mBlockTypeName; }
+ std::shared_ptr<cBlockHandler> handler() const { return mHandler; }
+
+
+private:
+
+ /** The name of the plugin that registered the block. */
+ AString mPluginName;
+
+ /** The name of the block type, such as "minecraft:redstone_lamp" */
+ AString mBlockTypeName;
+
+ /** The callbacks to call for various interaction. */
+ std::shared_ptr<cBlockHandler> mHandler;
+
+ /** Optional hints for any subsystem to use, such as "IsSnowable" -> "1". */
+ std::map<AString, AString> mHints;
+
+ /** The callbacks for dynamic evaluation of hints, such as "LightValue" -> function(BlockTypeName, BlockState). */
+ std::map<AString, HintCallback> mHintCallbacks;
+};
+
+
+
+
+
+/** Stores information on all known block types.
+Can dynamically add and remove block types.
+Block types are identified using BlockTypeName.
+Supports unregistering and re-registering the same type by the same plugin.
+Stores the name of the plugin that registered the type, for better plugin error messages ("already registered in X")
+and so that we can unload and reload plugins. */
+class BlockTypeRegistry
+{
+public:
+ // fwd:
+ class AlreadyRegisteredException;
+
+
+ /** Creates an empty new instance of the block type registry */
+ BlockTypeRegistry() = default;
+
+ /** Registers the specified block type.
+ If the block type already exists and the plugin is the same, updates the registration.
+ If the block type already exists and the plugin is different, throws an AlreadyRegisteredException. */
+ void registerBlockType(
+ const AString & aPluginName,
+ const AString & aBlockTypeName,
+ std::shared_ptr<cBlockHandler> aHandler,
+ const std::map<AString, AString> & aHints = std::map<AString, AString>(),
+ const std::map<AString, BlockInfo::HintCallback> & aHintCallbacks = std::map<AString, BlockInfo::HintCallback>()
+ );
+
+ /** Returns the registration information for the specified BlockTypeName.
+ Returns nullptr if BlockTypeName not found. */
+ std::shared_ptr<BlockInfo> blockInfo(const AString & aBlockTypeName);
+
+ /** Removes all registrations done by the specified plugin. */
+ void removeAllByPlugin(const AString & aPluginName);
+
+
+private:
+
+ /** The actual block type registry.
+ Maps the BlockTypeName to the BlockInfo instance. */
+ std::map<AString, std::shared_ptr<BlockInfo>> mRegistry;
+
+ /** The CS that protects mRegistry against multithreaded access. */
+ cCriticalSection mCSRegistry;
+};
+
+
+
+
+
+/** The exception thrown from BlockTypeRegistry::registerBlockType() if the same block type is being registered from a different plugin. */
+class BlockTypeRegistry::AlreadyRegisteredException: public std::runtime_error
+{
+ using Super = std::runtime_error;
+
+public:
+
+ /** Creates a new instance of the exception that provides info on both the original registration and the newly attempted
+ registration that caused the failure. */
+ AlreadyRegisteredException(
+ std::shared_ptr<BlockInfo> aPreviousRegistration,
+ std::shared_ptr<BlockInfo> aNewRegistration
+ );
+
+ // Simple getters:
+ std::shared_ptr<BlockInfo> previousRegistration() const { return mPreviousRegistration; }
+ std::shared_ptr<BlockInfo> newRegistration() const { return mNewRegistration; }
+
+
+private:
+
+ std::shared_ptr<BlockInfo> mPreviousRegistration;
+ std::shared_ptr<BlockInfo> mNewRegistration;
+
+
+ /** Returns the general exception message formatted by the two registrations.
+ The output is used when logging. */
+ static AString message(
+ std::shared_ptr<BlockInfo> aPreviousRegistration,
+ std::shared_ptr<BlockInfo> aNewRegistration
+ );
+};
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index e895b1657..75eb112c0 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -17,6 +17,7 @@ SET (SRCS
BlockArea.cpp
BlockID.cpp
BlockInfo.cpp
+ BlockTypeRegistry.cpp
BrewingRecipes.cpp
Broadcaster.cpp
BoundingBox.cpp
@@ -84,6 +85,7 @@ SET (HDRS
BlockInServerPluginInterface.h
BlockInfo.h
BlockTracer.h
+ BlockTypeRegistry.h
BrewingRecipes.h
BoundingBox.h
BuildInfo.h
diff --git a/tests/BlockTypeRegistry/BlockTypeRegistryTest.cpp b/tests/BlockTypeRegistry/BlockTypeRegistryTest.cpp
new file mode 100644
index 000000000..c2d8717e5
--- /dev/null
+++ b/tests/BlockTypeRegistry/BlockTypeRegistryTest.cpp
@@ -0,0 +1,228 @@
+
+#include "Globals.h"
+#include <thread>
+#include "BlockTypeRegistry.h"
+#include "../TestHelpers.h"
+
+
+
+
+/** Dummy BlockState implementation */
+class BlockState
+{
+public:
+ BlockState() = default;
+};
+
+
+
+
+/** Dummy cBlockHandler implementation that allows simple checking for equality through mIdent. */
+class cBlockHandler
+{
+public:
+ cBlockHandler(UInt32 aIdent):
+ mIdent(aIdent)
+ {
+ }
+
+ UInt32 mIdent;
+};
+
+
+
+
+
+/** Tests simple block type name registration.
+Registers a block type, checks that the type is then registered. */
+static void testSimpleReg()
+{
+ LOGD("Testing simple registration...");
+
+ // Register the block type:
+ BlockTypeRegistry reg;
+ AString blockTypeName("test:block1");
+ AString pluginName("testPlugin");
+ AString hint1("testHint1");
+ AString hint1Value("value1");
+ std::shared_ptr<cBlockHandler> handler(new cBlockHandler(0x12345678));
+ std::map<AString, AString> hints = {{hint1, hint1Value}, {"testHint2", "value2"}};
+ std::map<AString, BlockInfo::HintCallback> hintCallbacks;
+ reg.registerBlockType(pluginName, blockTypeName, handler, hints, hintCallbacks);
+
+ // Query the registration:
+ auto blockInfo = reg.blockInfo(blockTypeName);
+ TEST_NOTEQUAL(blockInfo, nullptr);
+ TEST_EQUAL(blockInfo->blockTypeName(), blockTypeName);
+ TEST_EQUAL(blockInfo->pluginName(), pluginName);
+ TEST_EQUAL(blockInfo->handler(), handler);
+ TEST_EQUAL(blockInfo->hintValue(hint1, BlockState()), hint1Value);
+ TEST_EQUAL(blockInfo->hintValue("nonexistent", BlockState()), "");
+}
+
+
+
+
+
+/** Tests that the plugin-based information is used correctly for registration.
+Registers two different block types with two different plugins, then tries to re-register them from a different plugin.
+Finally removes the registration through removeAllByPlugin() and checks its success. */
+static void testPlugins()
+{
+ LOGD("Testing plugin-based checks / removal...");
+
+ // Register the block types:
+ BlockTypeRegistry reg;
+ AString blockTypeName1("test:block1");
+ AString pluginName1("testPlugin1");
+ AString hint1("testHint1");
+ AString hint1Value("value1");
+ std::shared_ptr<cBlockHandler> handler1(new cBlockHandler(1));
+ std::map<AString, AString> hints = {{hint1, hint1Value}, {"testHint2", "value2"}};
+ std::map<AString, BlockInfo::HintCallback> hintCallbacks;
+ reg.registerBlockType(pluginName1, blockTypeName1, handler1, hints, hintCallbacks);
+ AString blockTypeName2("test:block2");
+ AString pluginName2("testPlugin2");
+ std::shared_ptr<cBlockHandler> handler2(new cBlockHandler(2));
+ reg.registerBlockType(pluginName2, blockTypeName2, handler2, hints, hintCallbacks);
+
+ // Test the refusal to register under a different plugin:
+ TEST_THROWS(reg.registerBlockType(pluginName2, blockTypeName1, handler2, hints, hintCallbacks), BlockTypeRegistry::AlreadyRegisteredException);
+ TEST_EQUAL(reg.blockInfo(blockTypeName1)->handler()->mIdent, 1); // Did we overwrite the old registration?
+ reg.registerBlockType(pluginName1, blockTypeName1, handler1, hints, hintCallbacks); // Re-registering must succeed
+
+ // Unregister by plugin, then re-register from a different plugin:
+ reg.removeAllByPlugin(pluginName1);
+ TEST_EQUAL(reg.blockInfo(blockTypeName1), nullptr); // Unregistered successfully
+ TEST_NOTEQUAL(reg.blockInfo(blockTypeName2), nullptr); // Didn't unregister from the other plugin
+ std::shared_ptr<cBlockHandler> handler3(new cBlockHandler(3));
+ reg.registerBlockType(pluginName2, blockTypeName1, handler3, hints, hintCallbacks);
+ TEST_NOTEQUAL(reg.blockInfo(blockTypeName1), nullptr); // Registered successfully
+ TEST_EQUAL(reg.blockInfo(blockTypeName1)->pluginName(), pluginName2);
+ TEST_EQUAL(reg.blockInfo(blockTypeName1)->handler()->mIdent, 3);
+ TEST_EQUAL(reg.blockInfo(blockTypeName2)->handler()->mIdent, 2);
+ reg.removeAllByPlugin(pluginName2);
+ TEST_EQUAL(reg.blockInfo(blockTypeName1), nullptr); // Unregistered successfully
+ TEST_EQUAL(reg.blockInfo(blockTypeName2), nullptr); // Unregistered successfully
+}
+
+
+
+
+/** Tests that the callback-based hints work properly. */
+static void testHintCallbacks()
+{
+ LOGD("Testing hint callbacks...");
+
+ // Register the block type:
+ BlockTypeRegistry reg;
+ AString blockTypeName("test:block1");
+ AString pluginName("testPlugin");
+ AString hint1("testHint1");
+ AString hint1Value("value1");
+ AString hc1("hintCallback1");
+ int callbackCount = 0;
+ auto callback1 = [&callbackCount](const AString & aBlockType, const BlockState & aBlockState)
+ {
+ callbackCount = callbackCount + 1;
+ return aBlockType + "_hint";
+ };
+ std::shared_ptr<cBlockHandler> handler(new cBlockHandler(0x12345678));
+ std::map<AString, AString> hints = {{hint1, hint1Value}, {"testHint2", "value2"}};
+ std::map<AString, BlockInfo::HintCallback> hintCallbacks = {{hc1, callback1}};
+ reg.registerBlockType(pluginName, blockTypeName, handler, hints, hintCallbacks);
+
+ // Check that querying the hint using a callback works:
+ TEST_EQUAL(reg.blockInfo(blockTypeName)->hintValue(hc1, BlockState()), blockTypeName + "_hint");
+ TEST_EQUAL(callbackCount, 1); // Called exactly once
+}
+
+
+
+
+
+/** Tests whether thread-locking works properly by running two threads,
+one constantly (re-)registering and the other one constantly querying the same block type. */
+static void testThreadLocking()
+{
+ LOGD("Testing thread locking...");
+
+ // Register the block type:
+ BlockTypeRegistry reg;
+ AString blockTypeName("test:block1");
+ AString pluginName("testPlugin");
+ AString hint1("testHint1");
+ AString hint1Value("value1");
+ std::shared_ptr<cBlockHandler> handler(new cBlockHandler(0x12345678));
+ std::map<AString, AString> hints = {{hint1, hint1Value}, {"testHint2", "value2"}};
+ std::map<AString, BlockInfo::HintCallback> hintCallbacks;
+ reg.registerBlockType(pluginName, blockTypeName, handler, hints, hintCallbacks);
+
+ // Run the two threads for at least a second:
+ auto endTime = time(nullptr) + 2;
+ auto keepRegistering = [&]()
+ {
+ while (time(nullptr) < endTime)
+ {
+ reg.registerBlockType(pluginName, blockTypeName, handler, hints, hintCallbacks);
+ }
+ };
+ auto keepQuerying = [&]()
+ {
+ unsigned numQueries = 0;
+ while (time(nullptr) < endTime)
+ {
+ TEST_NOTEQUAL(reg.blockInfo(blockTypeName), nullptr);
+ numQueries += 1;
+ }
+ LOGD("%u queries have been executed", numQueries);
+ };
+ std::thread thr1(keepRegistering);
+ std::thread thr2(keepQuerying);
+ thr1.join();
+ thr2.join();
+}
+
+
+
+
+
+static void testBlockTypeRegistry()
+{
+ testSimpleReg();
+ testPlugins();
+ testHintCallbacks();
+ testThreadLocking();
+}
+
+
+
+
+
+int main()
+{
+ LOGD("BlockTypeRegistryTest started");
+
+ try
+ {
+ testBlockTypeRegistry();
+ }
+ catch (const TestException & exc)
+ {
+ LOGERROR("BlockTypeRegistryTest has failed, an unhandled exception was thrown: %s", exc.mMessage.c_str());
+ return 1;
+ }
+ catch (...)
+ {
+ LOGERROR("BlockTypeRegistryTest has failed, an unhandled exception was thrown.");
+ return 1;
+ }
+
+ LOGD("BlockTypeRegistryTest finished");
+
+ return 0;
+}
+
+
+
+
diff --git a/tests/BlockTypeRegistry/CMakeLists.txt b/tests/BlockTypeRegistry/CMakeLists.txt
new file mode 100644
index 000000000..25b18c373
--- /dev/null
+++ b/tests/BlockTypeRegistry/CMakeLists.txt
@@ -0,0 +1,40 @@
+cmake_minimum_required(VERSION 3.0.2)
+enable_testing()
+
+include_directories(${CMAKE_SOURCE_DIR}/src/)
+
+add_definitions(-DTEST_GLOBALS=1)
+
+
+
+
+
+# Define individual test executables:
+
+# BlockTypeRegistryTest: Verify that the BlockTypeRegistry class works as intended:
+add_executable(BlockTypeRegistryTest
+ BlockTypeRegistryTest.cpp
+ ../TestHelpers.h
+ ${CMAKE_SOURCE_DIR}/src/BlockTypeRegistry.cpp
+ ${CMAKE_SOURCE_DIR}/src/StringUtils.cpp
+ ${CMAKE_SOURCE_DIR}/src/OSSupport/CriticalSection.cpp
+)
+target_link_libraries(BlockTypeRegistryTest fmt::fmt)
+
+
+
+
+
+# Define individual tests:
+
+add_test(NAME BlockTypeRegistryTest COMMAND BlockTypeRegistryTest)
+
+
+
+
+
+# Put all the tests into a solution folder (MSVC):
+set_target_properties(
+ BlockTypeRegistryTest
+ PROPERTIES FOLDER Tests/BlockTypeRegistry
+)
diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt
index 20ae1bfa3..74e4323ec 100644
--- a/tests/CMakeLists.txt
+++ b/tests/CMakeLists.txt
@@ -6,6 +6,7 @@ endif()
add_definitions(-DTEST_GLOBALS=1)
+add_subdirectory(BlockTypeRegistry)
add_subdirectory(BoundingBox)
add_subdirectory(ByteBuffer)
add_subdirectory(ChunkData)
diff --git a/tests/TestHelpers.h b/tests/TestHelpers.h
new file mode 100644
index 000000000..2e43e1e0d
--- /dev/null
+++ b/tests/TestHelpers.h
@@ -0,0 +1,79 @@
+// Helper macros for writing exception-based tests
+
+
+
+
+/** The exception that is thrown if a test fails.
+It doesn't inherit from any type so that it is not possible to catch it by a mistake,
+it needs to be caught explicitly (used in the TEST_THROWS).
+It bears a single message that is to be displayed to stderr. */
+class TestException
+{
+public:
+ TestException(const AString & aMessage):
+ mMessage(aMessage)
+ {
+ }
+
+ AString mMessage;
+};
+
+
+
+
+
+/** Checks that the two values are equal; if not, throws a TestException. */
+#define TEST_EQUAL(VAL1, VAL2) \
+ if (VAL1 != VAL2) \
+ { \
+ throw TestException(Printf("%s (line %d): Equality test failed: %s != %s", \
+ __FUNCTION__, __LINE__, \
+ #VAL1, #VAL2 \
+ )); \
+ }
+
+
+
+/** Checks that the two values are not equal; if they are, throws a TestException. */
+#define TEST_NOTEQUAL(VAL1, VAL2) \
+ if (VAL1 == VAL2) \
+ { \
+ throw TestException(Printf("%s (line %d): Inequality test failed: %s == %s", \
+ __FUNCTION__, __LINE__, \
+ #VAL1, #VAL2 \
+ )); \
+ }
+
+
+
+/** Checks that the statement throws an exception of the specified class. */
+#define TEST_THROWS(Stmt, ExcClass) \
+ try \
+ { \
+ Stmt; \
+ throw TestException(Printf("%s (line %d): Failed to throw an exception of type %s", \
+ __FUNCTION__, __LINE__, \
+ #ExcClass \
+ )); \
+ } \
+ catch (const ExcClass &) \
+ { \
+ /* This is the expected case. */ \
+ } \
+ catch (const std::exception & exc) \
+ { \
+ throw TestException(Printf("%s (line %d): An unexpected std::exception descendant was thrown, was expecting type %s. Message is: %s", \
+ __FUNCTION__, __LINE__, \
+ #ExcClass, exc.what() \
+ )); \
+ } \
+ catch (...) \
+ { \
+ throw TestException(Printf("%s (line %d): An unexpected exception object was thrown, was expecting type %s", \
+ __FUNCTION__, __LINE__, \
+ #ExcClass \
+ )); \
+ }
+
+
+