1
0

BlockTypeRegistry: Initial skeleton

This commit is contained in:
Mattes D 2019-01-23 20:54:29 +01:00
parent c00b365617
commit 3722a239bf
7 changed files with 661 additions and 0 deletions

153
src/BlockTypeRegistry.cpp Normal file
View File

@ -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()
);
}

158
src/BlockTypeRegistry.h Normal file
View File

@ -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
);
};

View File

@ -17,6 +17,7 @@ SET (SRCS
BlockArea.cpp BlockArea.cpp
BlockID.cpp BlockID.cpp
BlockInfo.cpp BlockInfo.cpp
BlockTypeRegistry.cpp
BrewingRecipes.cpp BrewingRecipes.cpp
Broadcaster.cpp Broadcaster.cpp
BoundingBox.cpp BoundingBox.cpp
@ -84,6 +85,7 @@ SET (HDRS
BlockInServerPluginInterface.h BlockInServerPluginInterface.h
BlockInfo.h BlockInfo.h
BlockTracer.h BlockTracer.h
BlockTypeRegistry.h
BrewingRecipes.h BrewingRecipes.h
BoundingBox.h BoundingBox.h
BuildInfo.h BuildInfo.h

View File

@ -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;
}

View File

@ -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
)

View File

@ -6,6 +6,7 @@ endif()
add_definitions(-DTEST_GLOBALS=1) add_definitions(-DTEST_GLOBALS=1)
add_subdirectory(BlockTypeRegistry)
add_subdirectory(BoundingBox) add_subdirectory(BoundingBox)
add_subdirectory(ByteBuffer) add_subdirectory(ByteBuffer)
add_subdirectory(ChunkData) add_subdirectory(ChunkData)

79
tests/TestHelpers.h Normal file
View File

@ -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 \
)); \
}