1
0
Fork 0

Add ProtocolBlockTypePalette (#4391)

This commit is contained in:
E14 2019-09-22 22:57:54 +02:00 committed by Mattes D
parent 70d0b46b60
commit d1c95742dd
15 changed files with 815 additions and 0 deletions

3
.gitmodules vendored
View File

@ -63,3 +63,6 @@
path = lib/fmt
url = https://github.com/fmtlib/fmt.git
ignore = dirty
[submodule "Tools/BlockTypePaletteGenerator/lib/lunajson"]
path = Tools/BlockTypePaletteGenerator/lib/lunajson
url = https://github.com/grafi-tt/lunajson.git

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,91 @@
-- lib/lunajson/src/ is not in default Lua package paths
package.path = 'lib/lunajson/src/?.lua;' .. package.path;
--- Prints usage instructions to stdout.
-- If the optional `message` is passed, output is prepended by message _and_
-- redirected to stderr.
function usage(message)
if message then
io.output(io.stderr);
io.write(message, "\n\n");
end
io.write(
"Usage: lua Generator.lua INPUTFILE OUTPUTFILE\n"..
"Converts the Minecraft blocks.json report format to the cuberite "..
"block type palette format.\n"..
"\n"..
"INPUTFILE and OUTPUTFILE must point to a valid path. INPUTFILE must "..
"be readable and OUTPUTFILE must be writable. Either can be replaced "..
"with `-` (dash character) to point to standard-input or -output.\n");
os.exit(message and 1 or 0);
end
-- Test whether the script is run in a path where it can load it's libraries
if not pcall(function() require("lunajson.decoder") end) then
usage("Could not load required libraries, please run `Generator.lua` "..
"within its directory and make sure to run `git submodule update`.");
end
-- Check/Prepare CLI arguments
local inpath, outpath = ...;
io.input(io.stdin);
io.output(io.stdout);
if select("#", ...) ~= 2 then
usage("Incorrect number of arguments.");
end
if inpath ~= "-" then
local handle, err = io.open(inpath, "r");
io.input(handle or usage(err));
end
if outpath ~= "-" then
local handle, err = io.open(outpath, "w");
io.output(handle or usage(err));
end
-- Main program starts here
local decode = (require("lunajson.decoder"))();
local encode = (require("lunajson.encoder"))();
local input = decode(io.input():read("*a"));
local registry = {};
local max_id = -1;
for blockname, blockdata in pairs(input) do
for i = 1, #(blockdata.states or {}) do
local state = blockdata.states[i];
assert(registry[state.id + 1] == nil, "Ensure no duplicate IDs");
-- needed in the end to verify we got no holes in the array:
max_id = math.max(max_id, state.id);
registry[state.id + 1] = {
id = assert(state.id, "id is required."),
name = assert(blockname, "Block type name is required."),
-- default = state.default or nil, -- may need this later
props = state.properties,
};
end
end
-- The following assertion is not necessary by the current spec, but is required
-- by how lunajson distinguishes objects from arrays. Also if this fails, it is
-- _very_ likely that the input file is faulty.
assert(#registry == max_id + 1, "Ensure that registry has contiguous keys");
local out = {
Metadata = {
ProtocolBlockTypePaletteVersion = 1
},
Palette = registry
};
io.write(encode(out), "\n");

View File

@ -0,0 +1,63 @@
This generator crafts an intermediate index format to be read by cuberite
# Running
Run `lua ./Generator.lua`, pass `blocks.json` as first argument to the script
and the desired output location as 2nd argument.
Make sure to run the Generator from within its directory (`cd` into the path
where `Generator.lua` is.)
## Examples
```bash
SERVER=/path/to/server.jar
java -cp "$SERVER" net.minecraft.data.Main --reports &&
lua Generator.lua \
generated/reports/blocks.json \
../../Server/Protocol/1.13/ProtocolBlockTypePalette.json
```
```bash
SERVER=/path/to/server.jar
java -cp "$SERVER" net.minecraft.data.Main --reports &&
lua Generator.lua - -\
< generated/reports/blocks.json \
> ../Server/Protocol/1.13/ProtocolBlockTypePalette.json
```
## Output format
The Format is a `JSON` document containing an object with at least two keys at
the top level: `Metadata` and `Palette`.
`Metadata` contains document metadata, namely a key `"ProtocolBlockType": 1`.
`Palette` contains an array of objects. Each of these objects has at least the
keys `id`, `name` and an optional `props` key that contains the individual
properties of the current state. These properties are a KV dict of pure strings.
The order of the array or object elements is not significant. `id` is unique.
```json
{
"Metadata": {
"ProtocolBlockType": 1
},
"Palette": [{
"id": 0,
"name": "minecraft:air"
}, {
"id": 1,
"name": "minecraft:stone"
}, {
"id": 221,
"name": "minecraft:dark_oak_leaves",
"props": {
"persistent": "false",
"distance": "4"
}
}
]
}
```

@ -0,0 +1 @@
Subproject commit 1bdc886a9abb1be745fa7436f2a043b0455d70b8

View File

@ -83,6 +83,52 @@ BlockState::BlockState(const BlockState & aCopyFrom, const std::map<AString, ASt
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:

View File

@ -52,6 +52,9 @@ public:
(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;

View File

@ -1,3 +1,4 @@
include_directories (SYSTEM "../../lib/jsoncpp/include")
SET (SRCS
Authenticator.cpp
@ -12,6 +13,7 @@ SET (SRCS
Protocol_1_12.cpp
Protocol_1_13.cpp
ProtocolRecognizer.cpp
ProtocolBlockTypePalette.cpp
)
SET (HDRS
@ -28,6 +30,7 @@ SET (HDRS
Protocol_1_12.h
Protocol_1_13.h
ProtocolRecognizer.h
ProtocolBlockTypePalette.h
)
if (NOT MSVC)

View File

@ -0,0 +1,144 @@
#include "Globals.h"
#include "ProtocolBlockTypePalette.h"
#include <cstdint>
#include <sstream>
#include "json/value.h"
#include "json/reader.h"
ProtocolBlockTypePalette::ProtocolBlockTypePalette()
{
// empty
}
bool ProtocolBlockTypePalette::loadFromString(const AString & aMapping)
{
std::stringstream stream;
stream << aMapping;
return loadFromStream(stream);
}
bool ProtocolBlockTypePalette::loadFromStream(std::istream & aInputStream)
{
Json::Value root;
try
{
aInputStream >> root;
}
#if defined _DEBUG
catch (const std::exception & e)
{
LOGD(e.what());
return false;
}
#else
catch (const std::exception &)
{
return false;
}
#endif
if (!root.isObject() ||
!root.isMember("Metadata") ||
!root["Metadata"].isMember("ProtocolBlockTypePaletteVersion") ||
!root.isMember("Palette") ||
!root["Palette"].isArray())
{
LOGD("Incorrect palette format.");
return false;
}
if (root["Metadata"]["ProtocolBlockTypePaletteVersion"].asUInt() != 1)
{
LOGD("Palette format version not supported.");
return false;
}
auto len = root["Palette"].size();
for (decltype(len) i = 0; i < len; ++i)
{
const auto & record = root["Palette"][i];
if (!record.isObject())
{
LOGD("Record #%u must be a JSON object.", i);
return false;
}
auto blocktype = record["name"].asString();
auto id = std::stoul(record["id"].asString());
std::map<AString, AString> state;
if (id >= NOT_FOUND)
{
LOGD("`id` must be less than ProtocolBlockTypePalette::NOT_FOUND, but is %lu", id);
return false;
}
if (record.isMember("props"))
{
const auto & props = record["props"];
if (!props.isObject())
{
LOGD("`props` key must be a JSON object.");
return false;
}
for (const auto & key: props.getMemberNames())
{
state[key] = props[key].asString();
}
}
// Block type map entry already exists?
if (mIndex.count(blocktype) == 0)
{
mIndex.insert({blocktype, std::map<BlockState, UInt32>()});
}
const auto & result = mIndex[blocktype].insert({BlockState(state), id});
if (result.second == false)
{
LOGINFO("Duplicate block state encountered (Current ID: %lu, other: %lu)", result.first->second, id);
}
}
return true;
}
UInt32 ProtocolBlockTypePalette::index(const AString & aBlockTypeName, const BlockState & aBlockState) const
{
auto a = mIndex.find(aBlockTypeName);
if (a != mIndex.end())
{
auto b = a->second.find(aBlockState);
if (b != a->second.end())
{
return b->second;
}
}
return NOT_FOUND;
}
void ProtocolBlockTypePalette::clear()
{
return mIndex.clear();
}

View File

@ -0,0 +1,40 @@
#pragma once
#include <unordered_map>
#include "../BlockState.h"
/** Parses and holds a collection of block types and their possible states
together with their corresponding Id within the Minecraft network protocol. */
class ProtocolBlockTypePalette
{
public:
static const UInt32 NOT_FOUND = UINT32_MAX;
/** Create a new empty instance. */
ProtocolBlockTypePalette();
/** Loads the palette from a string.
See loadFromStream() for further details. */
bool loadFromString(const AString & aMapping);
/** Loads the palette from an input stream.
Returns `true` on success, `false` otherwise. Sucessive calls to this method
will _add_ data to the palette. If duplicate keys are encountered, they will
be ignored and an info message logged. */
bool loadFromStream(std::istream & aInputStream);
/** Returns the defined index corresponding of the given aBlockTypeName and
aBlockState.
Returns ProtocolBlockTypePalette::NOT_FOUND if the tuple is not found. */
UInt32 index(const AString & aBlockTypeName, const BlockState & aBlockState) const;
/** Clears the palette. */
void clear();
protected:
/** The palette index. Each item in the map represents a single block state
palette entry. The value is the block state ID. */
std::unordered_map<AString, std::map<BlockState, UInt32>> mIndex;
};

View File

@ -109,8 +109,66 @@ static void testReplacing()
/** Tests the comparison operator. */
static void testComparison()
{
LOGD("Testing comparison of BlockStates...");
// Simple property value tests
TEST_FALSE((BlockState({{"a", "a"}}) < BlockState({{"a", "a"}})));
TEST_FALSE((BlockState() < BlockState()));
TEST_TRUE((BlockState() < BlockState({{"foo", "bar"}})));
TEST_FALSE((BlockState({{"foo", "bar"}}) < BlockState()));
}
/** Tests the comparison operator using crafted data to defeat the checksum. */
static void testComparison2()
{
/* The following test ensures that items inserted in different order result
in the same map. I.e. that the < operator is stable. */
std::vector<BlockState> v;
std::map<BlockState, bool> map1;
std::map<BlockState, bool> map2;
for (int i = 0; i < 128; ++i)
{
v.push_back(BlockState({{std::string(1, static_cast<char>(0x1F)), std::string(1, static_cast<char>(i))}}));
v.push_back(BlockState({{std::string(1, static_cast<char>(0x10)), std::string(1, static_cast<char>(i | 0x80))},
{std::string(1, static_cast<char>(0x0F)), std::string(1, static_cast<char>(0x80))}}));
}
for (size_t i = 0; i < v.size(); ++i)
{
map1[v[i]] = true;
}
for (auto i = v.size(); i > 0; --i)
{
map2[v[i - 1]] = true;
}
// Check result
TEST_EQUAL(v.size(), 2 * 128);
TEST_EQUAL(map1.size(), v.size());
TEST_EQUAL(map1.size(), map2.size());
for (const auto & item: map1)
{
TEST_EQUAL(map1[item.first], map2[item.first]);
}
}
IMPLEMENT_TEST_MAIN("BlockStateTest",
testStaticCreation();
testDynamicCreation();
testReplacing();
testComparison();
testComparison2();
)

View File

@ -19,3 +19,4 @@ add_subdirectory(Network)
add_subdirectory(OSSupport)
add_subdirectory(SchematicFileSerializer)
add_subdirectory(UUID)
add_subdirectory(ProtocolBlockTypePalette)

View File

@ -0,0 +1,47 @@
cmake_minimum_required(VERSION 3.0.2)
enable_testing()
add_definitions(-DTEST_GLOBALS=1)
include_directories(SYSTEM "../../lib/jsoncpp/include")
include_directories(${CMAKE_SOURCE_DIR}/src/)
add_definitions(-DTEST_GLOBALS=1)
set (SHARED_SRCS
${CMAKE_SOURCE_DIR}/src/Protocol/ProtocolBlockTypePalette.cpp
${CMAKE_SOURCE_DIR}/src/BlockState.cpp
${CMAKE_SOURCE_DIR}/src/StringUtils.cpp
)
set (SHARED_HDRS
../TestHelpers.h
${CMAKE_SOURCE_DIR}/src/Protocol/ProtocolBlockTypePalette.h
${CMAKE_SOURCE_DIR}/src/BlockState.h
${CMAKE_SOURCE_DIR}/src/StringUtils.h
)
set (SRCS
ProtocolBlockTypePaletteTest.cpp
)
file (COPY
test.btp.json
../../Server/Protocol/1.13/base.btp.json
DESTINATION ./)
source_group("Shared" FILES ${SHARED_SRCS} ${SHARED_HDRS})
source_group("Sources" FILES ${SRCS})
add_executable(ProtocolBlockTypePaletteTest-exe ${SRCS} ${SHARED_SRCS} ${SHARED_HDRS})
target_link_libraries(ProtocolBlockTypePaletteTest-exe fmt::fmt jsoncpp_lib_static)
add_test(NAME ProtocolBlockTypePaletteTest-test COMMAND ProtocolBlockTypePaletteTest-exe)
# Put the projects into solution folders (MSVC):
set_target_properties(
ProtocolBlockTypePaletteTest-exe
PROPERTIES FOLDER Tests
)

View File

@ -0,0 +1,168 @@
// ProtocolBlockTypePaletteTest.cpp
#include <string>
#include <fstream>
#include <streambuf>
#include "Globals.h"
#include "Protocol/ProtocolBlockTypePalette.h"
#include "../TestHelpers.h"
static void TestSuccess(void)
{
LOG("Test TestSuccess");
ProtocolBlockTypePalette palette;
auto example = "{\"Metadata\":{\"ProtocolBlockTypePaletteVersion\":1}, \"Palette\":[{\
\"props\": {\
\"foo\": \"bar\"\
}, \
\"name\": \"b\", \
\"id\": \"0\"\
}]}";
palette.clear();
TEST_TRUE(palette.loadFromString(example));
TEST_EQUAL(palette.index("b", BlockState({{"foo", "bar"}})), 0);
TEST_EQUAL(palette.index("b", BlockState({{"foo", "baz"}})), ProtocolBlockTypePalette::NOT_FOUND);
TEST_EQUAL(palette.index("a", BlockState({{"foo", "bar"}})), ProtocolBlockTypePalette::NOT_FOUND);
}
static void TestErrors(void)
{
LOG("Test TestErrors");
ProtocolBlockTypePalette palette;
TEST_FALSE(palette.loadFromString(""));
palette.clear();
TEST_FALSE(palette.loadFromString("[]"));
palette.clear();
TEST_FALSE(palette.loadFromString("a = {}"));
palette.clear();
TEST_FALSE(palette.loadFromString("{x = 1}")); // Lua style
palette.clear();
TEST_FALSE(palette.loadFromString("$#^%&"));
}
static void TestComplex1(void)
{
LOG("Test TestComplex1");
ProtocolBlockTypePalette palette;
auto str = "{\"Metadata\":{\"ProtocolBlockTypePaletteVersion\":1}, \"Palette\":[{\
\"props\": {\
\"foo\": \"bar\", \
\"moo\": \"baz\"\
}, \
\"id\": \"0\", \
\"name\": \"b\"\
}, {\
\"props\": {\
\"foo\": \"baz\", \
\"moo\": \"bar\"\
}, \
\"id\": \"1\", \
\"name\": \"b\"\
}, {\
\"props\": {\
\"foo\": \"baz\", \
\"moo\": \"bar\"\
}, \
\"id\": \"1001\", \
\"name\": \"b\"\
}]}";
TEST_TRUE(palette.loadFromString(str)); // This should print info message about duplicate ID
TEST_EQUAL(palette.index("b", BlockState({{"foo","bar"}})), ProtocolBlockTypePalette::NOT_FOUND);
TEST_EQUAL(palette.index("b", BlockState({{"foo","bar"}, {"moo","baz"}})), 0);
TEST_EQUAL(palette.index("b", BlockState({{"foo","baz"}, {"moo","bar"}})), 1);
TEST_EQUAL(palette.index("c", BlockState({{"foo","baz"}, {"moo","bar"}})), ProtocolBlockTypePalette::NOT_FOUND);
}
static void TestComplex2(void)
{
LOG("Test TestComplex2");
ProtocolBlockTypePalette palette;
auto str = "{\"Metadata\":{\"ProtocolBlockTypePaletteVersion\":1}, \"Palette\":[{\
\"id\": \"0\", \
\"name\": \"a\"\
}, {\
\"id\": \"1\", \
\"name\": \"b\"\
}]}";
TEST_TRUE(palette.loadFromString(str));
TEST_EQUAL(palette.index("a", BlockState()), 0);
TEST_EQUAL(palette.index("b", BlockState({})), 1);
}
static void TestFile(void)
{
LOG("Test TestFile");
std::ifstream f("base.btp.json");
ProtocolBlockTypePalette palette;
TEST_TRUE(palette.loadFromStream(f));
// This is a bit problematic - the only permanently fixed block Id is air...
TEST_EQUAL(palette.index("minecraft:air", BlockState()), 0);
TEST_NOTEQUAL(palette.index("minecraft:stone", BlockState()), ProtocolBlockTypePalette::NOT_FOUND);
TEST_NOTEQUAL(palette.index("minecraft:dirt", BlockState()), ProtocolBlockTypePalette::NOT_FOUND);
}
static void TestFile2(void)
{
LOG("Test TestFile2");
std::ifstream f("test.btp.json");
ProtocolBlockTypePalette palette;
TEST_TRUE(palette.loadFromStream(f));
TEST_EQUAL(palette.index("minecraft:air", BlockState({})), 0);
TEST_EQUAL(palette.index("minecraft:stone", BlockState()), 1);
TEST_EQUAL(palette.index("minecraft:stone", BlockState()), 1);
TEST_EQUAL(palette.index(
"minecraft:dark_oak_leaves",
BlockState({{"persistent", "false"}, {"distance", "6"}})
), 225);
TEST_EQUAL(palette.index(
"minecraft:dark_oak_leaves",
BlockState({{"persistent", "false"}})
), ProtocolBlockTypePalette::NOT_FOUND);
}
IMPLEMENT_TEST_MAIN("ProtocolBlockTypePaletteTest",
TestSuccess();
TestErrors();
TestComplex1();
TestComplex2();
TestFile();
TestFile2();
)

View File

@ -0,0 +1,146 @@
{
"Metadata": {
"ProtocolBlockTypePaletteVersion": 1
},
"Palette": [{
"id": 0,
"name": "minecraft:air"
}, {
"id": 1,
"name": "minecraft:stone"
}, {
"id": 221,
"name": "minecraft:dark_oak_leaves",
"props": {
"persistent": "false",
"distance": "4"
}
}, {
"id": 222,
"name": "minecraft:dark_oak_leaves",
"props": {
"persistent": "true",
"distance": "5"
}
}, {
"id": 223,
"name": "minecraft:dark_oak_leaves",
"props": {
"persistent": "false",
"distance": "5"
}
}, {
"id": 224,
"name": "minecraft:dark_oak_leaves",
"props": {
"persistent": "true",
"distance": "6"
}
}, {
"id": 225,
"name": "minecraft:dark_oak_leaves",
"props": {
"persistent": "false",
"distance": "6"
}
}, {
"id": 226,
"name": "minecraft:dark_oak_leaves",
"props": {
"persistent": "true",
"distance": "7"
}
}, {
"id": 227,
"name": "minecraft:dark_oak_leaves",
"props": {
"persistent": "false",
"distance": "7"
}
}, {
"id": 9988,
"name": "minecraft:powered_rail",
"props": {
"powered": "true",
"shape": "north_south"
}
}, {
"id": 9989,
"name": "minecraft:powered_rail",
"props": {
"powered": "true",
"shape": "east_west"
}
}, {
"id": 9990,
"name": "minecraft:powered_rail",
"props": {
"powered": "true",
"shape": "ascending_east"
}
}, {
"id": 9991,
"name": "minecraft:powered_rail",
"props": {
"powered": "true",
"shape": "ascending_west"
}
}, {
"id": 9992,
"name": "minecraft:powered_rail",
"props": {
"powered": "true",
"shape": "ascending_north"
}
}, {
"id": 9993,
"name": "minecraft:powered_rail",
"props": {
"powered": "true",
"shape": "ascending_south"
}
}, {
"id": 9994,
"name": "minecraft:powered_rail",
"props": {
"powered": "false",
"shape": "north_south"
}
}, {
"id": 9995,
"name": "minecraft:powered_rail",
"props": {
"powered": "false",
"shape": "east_west"
}
}, {
"id": 9996,
"name": "minecraft:powered_rail",
"props": {
"powered": "false",
"shape": "ascending_east"
}
}, {
"id": 9997,
"name": "minecraft:powered_rail",
"props": {
"powered": "false",
"shape": "ascending_west"
}
}, {
"id": 9998,
"name": "minecraft:powered_rail",
"props": {
"powered": "false",
"shape": "ascending_north"
}
}, {
"id": 9999,
"name": "minecraft:powered_rail",
"props": {
"powered": "false",
"shape": "ascending_south"
}
}
]
}