summaryrefslogtreecommitdiffstats
path: root/src/Bindings
diff options
context:
space:
mode:
Diffstat (limited to 'src/Bindings')
-rw-r--r--src/Bindings/BlockState.cpp213
-rw-r--r--src/Bindings/BlockState.h93
-rw-r--r--src/Bindings/BlockTypePalette.cpp380
-rw-r--r--src/Bindings/BlockTypePalette.h138
-rw-r--r--src/Bindings/BlockTypeRegistry.cpp240
-rw-r--r--src/Bindings/BlockTypeRegistry.h216
6 files changed, 1280 insertions, 0 deletions
diff --git a/src/Bindings/BlockState.cpp b/src/Bindings/BlockState.cpp
new file mode 100644
index 000000000..8ee87c50f
--- /dev/null
+++ b/src/Bindings/BlockState.cpp
@@ -0,0 +1,213 @@
+#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-return this using checksum
+ if (mChecksum != aOther.mChecksum)
+ {
+ return (mChecksum < aOther.mChecksum);
+ }
+
+ // Can fast-return this due to how comparison works
+ if (mState.size() != aOther.mState.size())
+ {
+ return (mState.size() < aOther.mState.size());
+ }
+
+ auto itA = mState.begin();
+ auto itOther = aOther.mState.begin();
+
+ // don't need to check itOther, size checks above ensure size(A) == size(O)
+ while (itA != mState.end())
+ {
+ {
+ const auto cmp = itA->first.compare(itOther->first);
+ if (cmp != 0)
+ {
+ return (cmp < 0);
+ }
+ }
+ {
+ const auto cmp = itA->second.compare(itOther->second);
+ if (cmp != 0)
+ {
+ return (cmp < 0);
+ }
+ }
+
+ ++itA;
+ ++itOther;
+ }
+
+ return false;
+}
+
+
+
+
+
+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/Bindings/BlockState.h b/src/Bindings/BlockState.h
new file mode 100644
index 000000000..ab451236b
--- /dev/null
+++ b/src/Bindings/BlockState.h
@@ -0,0 +1,93 @@
+#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);
+
+ /** Less-than comparison. */
+ bool operator <(const BlockState & aOther) const;
+
+ /** 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/Bindings/BlockTypePalette.cpp b/src/Bindings/BlockTypePalette.cpp
new file mode 100644
index 000000000..7759505cf
--- /dev/null
+++ b/src/Bindings/BlockTypePalette.cpp
@@ -0,0 +1,380 @@
+#include "Globals.h"
+#include "BlockTypePalette.h"
+#include "json/value.h"
+#include "JsonUtils.h"
+
+
+
+
+
+/** Returns the index into aString >= aStartIdx at which the next separator occurs.
+Separator is one of \t, \n or \r.
+Returns AString::npos if no such separator. */
+static size_t findNextSeparator(const AString & aString, size_t aStartIdx = 0)
+{
+ for (size_t i = aStartIdx, len = aString.length(); i < len; ++i)
+ {
+ switch (aString[i])
+ {
+ case '\t':
+ case '\n':
+ case '\r':
+ {
+ return i;
+ }
+ }
+ }
+ return AString::npos;
+}
+
+
+
+
+
+BlockTypePalette::BlockTypePalette():
+ mMaxIndex(0)
+{
+}
+
+
+
+
+
+UInt32 BlockTypePalette::index(const AString & aBlockTypeName, const BlockState & aBlockState)
+{
+ auto idx = maybeIndex(aBlockTypeName, aBlockState);
+ if (idx.second)
+ {
+ return idx.first;
+ }
+
+ // Not found, append:
+ auto index = mMaxIndex++;
+ mBlockToNumber[aBlockTypeName][aBlockState] = index;
+ mNumberToBlock[index] = {aBlockTypeName, aBlockState};
+ return index;
+}
+
+
+
+
+
+std::pair<UInt32, bool> BlockTypePalette::maybeIndex(const AString & aBlockTypeName, const BlockState & aBlockState) const
+{
+ auto itr1 = mBlockToNumber.find(aBlockTypeName);
+ if (itr1 == mBlockToNumber.end())
+ {
+ return {0, false};
+ }
+ auto itr2 = itr1->second.find(aBlockState);
+ if (itr2 == itr1->second.end())
+ {
+ return {0, false};
+ }
+ return {itr2->second, true};
+}
+
+
+
+
+
+UInt32 BlockTypePalette::count() const
+{
+ return static_cast<UInt32>(mNumberToBlock.size());
+}
+
+
+
+
+
+const std::pair<AString, BlockState> & BlockTypePalette::entry(UInt32 aIndex) const
+{
+ auto itr = mNumberToBlock.find(aIndex);
+ if (itr == mNumberToBlock.end())
+ {
+ throw NoSuchIndexException(aIndex);
+ }
+ return itr->second;
+}
+
+
+
+
+
+std::map<UInt32, UInt32> BlockTypePalette::createTransformMapAddMissing(const BlockTypePalette & aFrom)
+{
+ std::map<UInt32, UInt32> res;
+ for (const auto & fromEntry: aFrom.mNumberToBlock)
+ {
+ auto fromIndex = fromEntry.first;
+ const auto & blockTypeName = fromEntry.second.first;
+ const auto & blockState = fromEntry.second.second;
+ res[fromIndex] = index(blockTypeName, blockState);
+ }
+ return res;
+}
+
+
+
+
+
+std::map<UInt32, UInt32> BlockTypePalette::createTransformMapWithFallback(const BlockTypePalette & aFrom, UInt32 aFallbackIndex) const
+{
+ std::map<UInt32, UInt32> res;
+ for (const auto & fromEntry: aFrom.mNumberToBlock)
+ {
+ auto fromIndex = fromEntry.first;
+ const auto & blockTypeName = fromEntry.second.first;
+ const auto & blockState = fromEntry.second.second;
+ auto thisIndex = maybeIndex(blockTypeName, blockState);
+ if (thisIndex.second)
+ {
+ // The entry was found in this
+ res[fromIndex] = thisIndex.first;
+ }
+ else
+ {
+ // The entry was NOT found in this, replace with fallback:
+ res[fromIndex] = aFallbackIndex;
+ }
+ }
+ return res;
+}
+
+
+
+
+
+void BlockTypePalette::loadFromString(const AString & aString)
+{
+ static const AString hdrTsvRegular = "BlockTypePalette";
+ static const AString hdrTsvUpgrade = "UpgradeBlockTypePalette";
+
+ // Detect format by checking the header line (none -> JSON):
+ if (aString.substr(0, hdrTsvRegular.length()) == hdrTsvRegular)
+ {
+ return loadFromTsv(aString, false);
+ }
+ else if (aString.substr(0, hdrTsvUpgrade.length()) == hdrTsvUpgrade)
+ {
+ return loadFromTsv(aString, true);
+ }
+ return loadFromJsonString(aString);
+}
+
+
+
+
+
+void BlockTypePalette::loadFromJsonString(const AString & aJsonPalette)
+{
+ // Parse the string into JSON object:
+ Json::Value root;
+ std::string errs;
+ if (!JsonUtils::ParseString(aJsonPalette, root, &errs))
+ {
+ throw LoadFailedException(errs);
+ }
+
+ // Sanity-check the JSON's structure:
+ if (!root.isObject())
+ {
+ throw LoadFailedException("Incorrect palette format, expected an object at root.");
+ }
+
+ // Load the palette:
+ for (auto itr = root.begin(), end = root.end(); itr != end; ++itr)
+ {
+ const auto & blockTypeName = itr.name();
+ const auto & states = (*itr)["states"];
+ if (states == Json::Value())
+ {
+ throw LoadFailedException(Printf("Missing \"states\" for block type \"%s\"", blockTypeName));
+ }
+ for (const auto & state: states)
+ {
+ auto id = static_cast<UInt32>(std::stoul(state["id"].asString()));
+ std::map<AString, AString> props;
+ if (state.isMember("properties"))
+ {
+ const auto & properties = state["properties"];
+ if (!properties.isObject())
+ {
+ throw LoadFailedException(Printf("Member \"properties\" is not a JSON object (block type \"%s\", id %u).", blockTypeName, id));
+ }
+ for (const auto & key: properties.getMemberNames())
+ {
+ props[key] = properties[key].asString();
+ }
+ }
+ addMapping(id, blockTypeName, props);
+ }
+ }
+}
+
+
+
+
+
+void BlockTypePalette::loadFromTsv(const AString & aTsvPalette, bool aIsUpgrade)
+{
+ static const AString hdrTsvRegular = "BlockTypePalette";
+ static const AString hdrTsvUpgrade = "UpgradeBlockTypePalette";
+
+ // Check the file signature:
+ auto idx = findNextSeparator(aTsvPalette);
+ if ((idx == AString::npos) || (aTsvPalette[idx] == '\t'))
+ {
+ throw LoadFailedException("Invalid signature");
+ }
+ auto signature = aTsvPalette.substr(0, idx);
+ bool isUpgrade = (signature == hdrTsvUpgrade);
+ if (!isUpgrade && (signature != hdrTsvRegular))
+ {
+ throw LoadFailedException("Unknown signature");
+ }
+ if (aTsvPalette[idx] == '\r') // CR of the CRLF pair, skip the LF:
+ {
+ idx += 1;
+ }
+
+ // Parse the header:
+ bool hasHadVersion = false;
+ AString commonPrefix;
+ int line = 2;
+ auto len = aTsvPalette.length();
+ while (true)
+ {
+ auto keyStart = idx + 1;
+ auto keyEnd = findNextSeparator(aTsvPalette, idx + 1);
+ if (keyEnd == AString::npos)
+ {
+ throw LoadFailedException(Printf("Invalid header key format on line %u", line));
+ }
+ if (keyEnd == idx + 1) // Empty line, end of headers
+ {
+ if (aTsvPalette[keyEnd] == '\r') // CR of the CRLF pair, skip the LF:
+ {
+ ++keyEnd;
+ }
+ idx = keyEnd;
+ ++line;
+ break;
+ }
+ auto valueEnd = findNextSeparator(aTsvPalette, keyEnd + 1);
+ if ((valueEnd == AString::npos) || (aTsvPalette[valueEnd] == '\t'))
+ {
+ throw LoadFailedException(Printf("Invalid header value format on line %u", line));
+ }
+ auto key = aTsvPalette.substr(keyStart, keyEnd - keyStart);
+ if (key == "FileVersion")
+ {
+ unsigned version = 0;
+ auto value = aTsvPalette.substr(keyEnd + 1, valueEnd - keyEnd - 1);
+ if (!StringToInteger(value, version))
+ {
+ throw LoadFailedException("Invalid FileVersion value");
+ }
+ else if (version != 1)
+ {
+ throw LoadFailedException(Printf("Unknown FileVersion: %u. Only version 1 is supported.", version));
+ }
+ hasHadVersion = true;
+ }
+ else if (key == "CommonPrefix")
+ {
+ commonPrefix = aTsvPalette.substr(keyEnd + 1, valueEnd - keyEnd - 1);
+ }
+ idx = valueEnd;
+ if (aTsvPalette[idx] == '\r') // CR of the CRLF pair, skip the LF:
+ {
+ ++idx;
+ }
+ ++line;
+ }
+ if (!hasHadVersion)
+ {
+ throw LoadFailedException("No FileVersion value");
+ }
+
+ // Parse the data:
+ while (idx + 1 < len)
+ {
+ auto lineStart = idx + 1;
+ auto idEnd = findNextSeparator(aTsvPalette, lineStart);
+ if ((idEnd == AString::npos) || (aTsvPalette[idEnd] != '\t'))
+ {
+ throw LoadFailedException(Printf("Incomplete data on line %u (id)", line));
+ }
+ UInt32 id;
+ if (!StringToInteger(aTsvPalette.substr(lineStart, idEnd - lineStart), id))
+ {
+ throw LoadFailedException(Printf("Failed to parse id on line %u", line));
+ }
+ size_t metaEnd = idEnd;
+ if (isUpgrade)
+ {
+ metaEnd = findNextSeparator(aTsvPalette, idEnd + 1);
+ if ((metaEnd == AString::npos) || (aTsvPalette[metaEnd] != '\t'))
+ {
+ throw LoadFailedException(Printf("Incomplete data on line %u (meta)", line));
+ }
+ UInt32 meta = 0;
+ if (!StringToInteger(aTsvPalette.substr(idEnd + 1, metaEnd - idEnd - 1), meta))
+ {
+ throw LoadFailedException(Printf("Failed to parse meta on line %u", line));
+ }
+ if (meta > 15)
+ {
+ throw LoadFailedException(Printf("Invalid meta value on line %u: %u", line, meta));
+ }
+ id = (id * 16) | meta;
+ }
+ auto blockTypeEnd = findNextSeparator(aTsvPalette, metaEnd + 1);
+ if (blockTypeEnd == AString::npos)
+ {
+ throw LoadFailedException(Printf("Incomplete data on line %u (blockTypeName)", line));
+ }
+ auto blockTypeName = aTsvPalette.substr(metaEnd + 1, blockTypeEnd - metaEnd - 1);
+ auto blockStateEnd = blockTypeEnd;
+ AStringMap blockState;
+ while (aTsvPalette[blockStateEnd] == '\t')
+ {
+ auto keyEnd = findNextSeparator(aTsvPalette, blockStateEnd + 1);
+ if ((keyEnd == AString::npos) || (aTsvPalette[keyEnd] != '\t'))
+ {
+ throw LoadFailedException(Printf("Incomplete data on line %u (blockState key)", line));
+ }
+ auto valueEnd = findNextSeparator(aTsvPalette, keyEnd + 1);
+ if (valueEnd == AString::npos)
+ {
+ throw LoadFailedException(Printf("Incomplete data on line %u (blockState value)", line));
+ }
+ auto key = aTsvPalette.substr(blockStateEnd + 1, keyEnd - blockStateEnd - 1);
+ auto value = aTsvPalette.substr(keyEnd + 1, valueEnd - keyEnd - 1);
+ blockState[key] = value;
+ blockStateEnd = valueEnd;
+ }
+ addMapping(id, commonPrefix + blockTypeName, std::move(blockState));
+ ++line;
+ if (aTsvPalette[blockStateEnd] == '\r') // CR of the CRLF pair, skip the LF:
+ {
+ ++blockStateEnd;
+ }
+ idx = blockStateEnd;
+ }
+}
+
+
+
+
+
+void BlockTypePalette::addMapping(UInt32 aID, const AString & aBlockTypeName, const BlockState & aBlockState)
+{
+ mNumberToBlock[aID] = {aBlockTypeName, aBlockState};
+ mBlockToNumber[aBlockTypeName][aBlockState] = aID;
+ if (aID > mMaxIndex)
+ {
+ mMaxIndex = aID;
+ }
+}
diff --git a/src/Bindings/BlockTypePalette.h b/src/Bindings/BlockTypePalette.h
new file mode 100644
index 000000000..2aade422b
--- /dev/null
+++ b/src/Bindings/BlockTypePalette.h
@@ -0,0 +1,138 @@
+#pragma once
+
+#include <utility>
+#include "BlockState.h"
+
+
+
+
+
+/** Holds a palette that maps between block type + state and numbers.
+Used primarily by PalettedBlockArea to map from stringular block representation to numeric,
+and by protocols to map from stringular block representation to protocol-numeric.
+The object itself provides no thread safety, users of this class need to handle locking, if required.
+Note that the palette itself doesn't support erasing;
+to erase, create a new instance and re-add only the wanted items.
+
+Internally, the object uses two synced maps, one for each translation direction.
+
+The palette can be loaded from a string (file). The loader supports either the blocks.json file exported by
+the vanilla server itself (https://wiki.vg/Data_Generators), or a processed text file generated by
+our tool $/Tools/BlockTypePaletteGenerator/, or a hand-written text file describing the upgrade from
+1.12 BlockType + BlockMeta to 1.13 string representations.
+The text file is a TSV (tab-separated values), which basically means the data is generally structured as
+<value1><tab><value2><tab><value3><tab>...<valueN><eol>, where eol is the platform's CR / CRLF / LF lineend.
+The file starts with a single value on the first line, "BlockTypePalette" or "UpgradeBlockTypePalette", which
+is used to detect the file format. The following lines are "headers", simple <key><tab><value><eol> entries
+that contain the metadata about the file. "FileVersion" is a compulsory key, "CommonPrefix" is supported, others
+are ignored.
+The headers are followed by an empty line (that signalizes the end of headers) and then the actual data.
+For regular BlockTypePalette TSV file of version 1, the data is in the format:
+<index><tab><blockTypeName><tab><state1Name><tab><state1Value><tab><state2Name> ... <eol>
+For the UpgradeBlockTypePalette TSV file of version 1, the data is in the format:
+<blockType><tab><blockMeta><tab><blockTypeName><tab><state1Name><tab><state1Value><tab><state2Name> ... <eol>
+If a CommonPrefix header is present, its value is pre-pended to each blockTypeName loaded (thus allowing
+the file to be overall smaller). */
+class BlockTypePalette
+{
+public:
+
+ /** Exception that is thrown if requiesting an index not present in the palette. */
+ class NoSuchIndexException:
+ public std::runtime_error
+ {
+ using Super = std::runtime_error;
+
+ public:
+ NoSuchIndexException(UInt32 aIndex):
+ Super(Printf("No such palette index: %u", aIndex))
+ {
+ }
+ };
+
+
+ /** Exception that is thrown when loading the palette fails hard (bad format). */
+ class LoadFailedException:
+ public std::runtime_error
+ {
+ using Super = std::runtime_error;
+
+ public:
+ LoadFailedException(const AString & aReason):
+ Super(aReason)
+ {
+ }
+ };
+
+
+
+ /** Create a new empty instance. */
+ BlockTypePalette();
+
+ /** Returns the index of the specified block type name and state.
+ If the combination is not found, it is added to the palette and the new index is returned. */
+ UInt32 index(const AString & aBlockTypeName, const BlockState & aBlockState);
+
+ /** Returns the <index, true> of the specified block type name and state, if it exists.
+ If the combination is not found, returns <undefined, false>. */
+ std::pair<UInt32, bool> maybeIndex(const AString & aBlockTypeName, const BlockState & aBlockState) const;
+
+ /** Returns the total number of entries in the palette. */
+ UInt32 count() const;
+
+ /** Returns the blockspec represented by the specified palette index.
+ If the index is not valid, throws a NoSuchIndexException. */
+ const std::pair<AString, BlockState> & entry(UInt32 aIndex) const;
+
+ /** Returns an index-transform map from aFrom to this (this.entry(idx) == aFrom.entry(res[idx])).
+ Entries from aFrom that are not present in this are added.
+ Used when pasting two areas, to transform the src palette to dst palette. */
+ std::map<UInt32, UInt32> createTransformMapAddMissing(const BlockTypePalette & aFrom);
+
+ /** Returns an index-transform map from aFrom to this (this.entry(idx) == aFrom.entry(res[idx])).
+ Entries from aFrom that are not present in this are assigned the fallback index.
+ Used for protocol block type mapping. */
+ std::map<UInt32, UInt32> createTransformMapWithFallback(const BlockTypePalette & aFrom, UInt32 aFallbackIndex) const;
+
+ /** Loads the palette from the string representation.
+ Throws a LoadFailedException if the loading fails hard (bad string format);
+ but still a part of the data may already be loaded at that point.
+ If the string specifies duplicate entries (either to already existing entries, or to itself),
+ the duplicates replace the current values silently (this allows us to chain multiple files as "overrides".
+ Auto-detects the string format (json / tsv, normal / upgrade palette) and calls the appropriate load function. */
+ void loadFromString(const AString & aString);
+
+
+protected:
+
+ /** The mapping from numeric to stringular representation.
+ mNumberToBlock[index] = {"blockTypeName", blockState}. */
+ std::map<UInt32, std::pair<AString, BlockState>> mNumberToBlock;
+
+ /** The mapping from stringular to numeric representation.
+ mStringToNumber["blockTypeName"][blockState] = index. */
+ std::unordered_map<AString, std::map<BlockState, UInt32>> mBlockToNumber;
+
+ /** The maximum index ever used in the maps.
+ Used when adding new entries through the index() call. */
+ UInt32 mMaxIndex;
+
+
+ /** Loads the palette from the JSON representation, https://wiki.vg/Data_Generators
+ Throws a LoadFailedException if the loading fails hard (bad string format);
+ but still a part of the data may already be loaded at that point.
+ See also: loadFromString(). */
+ void loadFromJsonString(const AString & aJsonPalette);
+
+ /** Loads the palette from the regular or upgrade TSV representation.
+ aIsUpgrade specifies whether the format is an upgrade TSV (true) or a regular one (false)
+ Throws a LoadFailedException if the loading fails hard (bad string format);
+ but still a part of the data may already be loaded at that point.
+ See also: loadFromString(). */
+ void loadFromTsv(const AString & aTsvPalette, bool aIsUpgrade);
+
+ /** Adds a mapping between the numeric and stringular representation into both maps,
+ updates the mMaxIndex, if appropriate.
+ Silently overwrites any previous mapping for the ID, if present, but keeps the old string->id mapping. */
+ void addMapping(UInt32 aID, const AString & aBlockTypeName, const BlockState & aBlockState);
+};
diff --git a/src/Bindings/BlockTypeRegistry.cpp b/src/Bindings/BlockTypeRegistry.cpp
new file mode 100644
index 000000000..491e03593
--- /dev/null
+++ b/src/Bindings/BlockTypeRegistry.cpp
@@ -0,0 +1,240 @@
+
+#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
+):
+ m_PluginName(aPluginName),
+ m_BlockTypeName(aBlockTypeName),
+ m_Handler(std::move(aHandler)),
+ m_Hints(aHints),
+ m_HintCallbacks(aHintCallbacks)
+{
+}
+
+
+
+
+
+AString BlockInfo::hintValue(
+ const AString & aHintName,
+ const BlockState & aBlockState
+)
+{
+ // Search the hint callbacks first:
+ auto itrC = m_HintCallbacks.find(aHintName);
+ if (itrC != m_HintCallbacks.end())
+ {
+ // Hint callback found, use it:
+ return itrC->second(m_BlockTypeName, aBlockState);
+ }
+
+ // Search the static hints:
+ auto itr = m_Hints.find(aHintName);
+ if (itr != m_Hints.end())
+ {
+ // Hint found, use it:
+ return itr->second;
+ }
+
+ // Nothing found, return empty string:
+ return AString();
+}
+
+
+
+
+
+void BlockInfo::setHint(const AString & aHintKey, const AString & aHintValue)
+{
+ m_Hints[aHintKey] = aHintValue;
+
+ // Warn if the hint is already provided by a callback (aHintValue will be ignored when evaluating the hint):
+ auto itrC = m_HintCallbacks.find(aHintKey);
+ if (itrC != m_HintCallbacks.end())
+ {
+ LOGINFO("Setting a static hint %s for block type %s, but there's already a callback for that hint. The static hint will be ignored.",
+ aHintKey.c_str(), m_BlockTypeName.c_str()
+ );
+ }
+}
+
+
+
+
+
+void BlockInfo::removeHint(const AString & aHintKey)
+{
+ m_Hints.erase(aHintKey);
+}
+
+
+
+
+
+////////////////////////////////////////////////////////////////////////////////
+// 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, std::move(aHandler), aHints, aHintCallbacks
+ );
+
+ // Check previous registrations:
+ cCSLock lock(m_CSRegistry);
+ auto itr = m_Registry.find(aBlockTypeName);
+ if (itr != m_Registry.end())
+ {
+ if (itr->second->pluginName() != aPluginName)
+ {
+ throw AlreadyRegisteredException(itr->second, blockInfo);
+ }
+ }
+
+ // Store the registration:
+ m_Registry[aBlockTypeName] = blockInfo;
+}
+
+
+
+
+
+std::shared_ptr<BlockInfo> BlockTypeRegistry::blockInfo(const AString & aBlockTypeName)
+{
+ cCSLock lock(m_CSRegistry);
+ auto itr = m_Registry.find(aBlockTypeName);
+ if (itr == m_Registry.end())
+ {
+ return nullptr;
+ }
+ return itr->second;
+}
+
+
+
+
+
+void BlockTypeRegistry::removeAllByPlugin(const AString & aPluginName)
+{
+ cCSLock lock(m_CSRegistry);
+ for (auto itr = m_Registry.begin(); itr != m_Registry.end();)
+ {
+ if (itr->second->pluginName() == aPluginName)
+ {
+ itr = m_Registry.erase(itr);
+ }
+ else
+ {
+ ++itr;
+ }
+ }
+}
+
+
+
+
+
+void BlockTypeRegistry::setBlockTypeHint(
+ const AString & aBlockTypeName,
+ const AString & aHintKey,
+ const AString & aHintValue
+)
+{
+ cCSLock lock(m_CSRegistry);
+ auto blockInfo = m_Registry.find(aBlockTypeName);
+ if (blockInfo == m_Registry.end())
+ {
+ throw NotRegisteredException(aBlockTypeName, aHintKey, aHintValue);
+ }
+ blockInfo->second->setHint(aHintKey, aHintValue);
+}
+
+
+
+
+
+void BlockTypeRegistry::removeBlockTypeHint(
+ const AString & aBlockTypeName,
+ const AString & aHintKey
+)
+{
+ cCSLock lock(m_CSRegistry);
+ auto blockInfo = m_Registry.find(aBlockTypeName);
+ if (blockInfo == m_Registry.end())
+ {
+ return;
+ }
+ blockInfo->second->removeHint(aHintKey);
+}
+
+
+
+
+
+////////////////////////////////////////////////////////////////////////////////
+// BlockTypeRegistry::AlreadyRegisteredException:
+
+BlockTypeRegistry::AlreadyRegisteredException::AlreadyRegisteredException(
+ const std::shared_ptr<BlockInfo> & aPreviousRegistration,
+ const std::shared_ptr<BlockInfo> & aNewRegistration
+) :
+ Super(message(aPreviousRegistration, aNewRegistration)),
+ m_PreviousRegistration(aPreviousRegistration),
+ m_NewRegistration(aNewRegistration)
+{
+}
+
+
+
+
+
+AString BlockTypeRegistry::AlreadyRegisteredException::message(
+ const std::shared_ptr<BlockInfo> & aPreviousRegistration,
+ const 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()
+ );
+}
+
+
+
+
+
+////////////////////////////////////////////////////////////////////////////////
+// BlockTypeRegistry::NotRegisteredException:
+
+BlockTypeRegistry::NotRegisteredException::NotRegisteredException(
+ const AString & aBlockTypeName,
+ const AString & aHintKey,
+ const AString & aHintValue
+):
+ Super(Printf(
+ "Attempting to set a hint of nonexistent BlockTypeName.\n\tBlockTypeName = %s\n\tHintKey = %s\n\tHintValue = %s",
+ aBlockTypeName.c_str(),
+ aHintKey.c_str(),
+ aHintValue.c_str()
+ ))
+{
+}
diff --git a/src/Bindings/BlockTypeRegistry.h b/src/Bindings/BlockTypeRegistry.h
new file mode 100644
index 000000000..3a85ee510
--- /dev/null
+++ b/src/Bindings/BlockTypeRegistry.h
@@ -0,0 +1,216 @@
+#pragma once
+
+
+
+
+
+#include <map>
+#include <functional>
+
+
+
+
+
+// fwd:
+class cBlockHandler;
+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 hint callbacks first, then static 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 m_PluginName; }
+ const AString & blockTypeName() const { return m_BlockTypeName; }
+ std::shared_ptr<cBlockHandler> handler() const { return m_Handler; }
+
+ /** Sets (creates or updates) a static hint.
+ Hints provided by callbacks are unaffected by this - callbacks are "higher priority", they overwrite anything set here.
+ Logs an info message if the hint is already provided by a hint callback. */
+ void setHint(const AString & aHintKey, const AString & aHintValue);
+
+ /** Removes a hint.
+ Silently ignored if the hint hasn't been previously set. */
+ void removeHint(const AString & aHintKey);
+
+
+private:
+
+ /** The name of the plugin that registered the block. */
+ AString m_PluginName;
+
+ /** The name of the block type, such as "minecraft:redstone_lamp" */
+ AString m_BlockTypeName;
+
+ /** The callbacks to call for various interaction. */
+ std::shared_ptr<cBlockHandler> m_Handler;
+
+ /** Optional static hints for any subsystem to use, such as "IsSnowable" -> "1".
+ Hint callbacks are of higher priority than m_Hints - if a hint is provided by a m_HintCallback, its value in m_Hints is ignored. */
+ std::map<AString, AString> m_Hints;
+
+ /** The callbacks for dynamic evaluation of hints, such as "LightValue" -> function(BlockTypeName, BlockState).
+ Hint callbacks are of higher priority than m_Hints - if a hint is provided by a m_HintCallback, its value in m_Hints is ignored. */
+ std::map<AString, HintCallback> m_HintCallbacks;
+};
+
+
+
+
+
+/** 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;
+ class NotRegisteredException;
+
+
+ /** 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);
+
+ /** Sets (adds or overwrites) a single Hint value for a BlockType.
+ Throws NotRegisteredException if the BlockTypeName is not registered. */
+ void setBlockTypeHint(
+ const AString & aBlockTypeName,
+ const AString & aHintKey,
+ const AString & aHintValue
+ );
+
+ /** Removes a previously registered single Hint value for a BlockType.
+ Throws NotRegisteredException if the BlockTypeName is not registered.
+ Silently ignored if the Hint hasn't been previously set. */
+ void removeBlockTypeHint(
+ const AString & aBlockTypeName,
+ const AString & aHintKey
+ );
+
+
+private:
+
+ /** The actual block type registry.
+ Maps the BlockTypeName to the BlockInfo instance. */
+ std::map<AString, std::shared_ptr<BlockInfo>> m_Registry;
+
+ /** The CS that protects m_Registry against multithreaded access. */
+ cCriticalSection m_CSRegistry;
+};
+
+
+
+
+
+/** 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(
+ const std::shared_ptr<BlockInfo> & aPreviousRegistration,
+ const std::shared_ptr<BlockInfo> & aNewRegistration
+ );
+
+ // Simple getters:
+ std::shared_ptr<BlockInfo> previousRegistration() const { return m_PreviousRegistration; }
+ std::shared_ptr<BlockInfo> newRegistration() const { return m_NewRegistration; }
+
+
+private:
+
+ std::shared_ptr<BlockInfo> m_PreviousRegistration;
+ std::shared_ptr<BlockInfo> m_NewRegistration;
+
+
+ /** Returns the general exception message formatted by the two registrations.
+ The output is used when logging. */
+ static AString message(
+ const std::shared_ptr<BlockInfo> & aPreviousRegistration,
+ const std::shared_ptr<BlockInfo> & aNewRegistration
+ );
+};
+
+
+
+
+
+/** The exception thrown from BlockTypeRegistry::setBlockTypeHint() if the block type has not been registered before. */
+class BlockTypeRegistry::NotRegisteredException: 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. */
+ NotRegisteredException(
+ const AString & aBlockTypeName,
+ const AString & aHintKey,
+ const AString & aHintValue
+ );
+
+ // Simple getters:
+ const AString & blockTypeName() const { return m_BlockTypeName; }
+
+
+private:
+
+ const AString m_BlockTypeName;
+};