diff --git a/Server/Plugins/APIDump/APIDesc.lua b/Server/Plugins/APIDump/APIDesc.lua index be9f60662..6be3795a8 100644 --- a/Server/Plugins/APIDump/APIDesc.lua +++ b/Server/Plugins/APIDump/APIDesc.lua @@ -1372,53 +1372,6 @@ local Item5 = cItem(E_ITEM_DIAMOND_CHESTPLATE, 1, 0, "thorns=1;unbreaking=3"); }, }, -- cItem - cObjective = - { - Desc = [[ - This class represents a single scoreboard objective. - ]], - Functions = - { - AddScore = { Params = "string, number", Return = "Score", Notes = "Adds a value to the score of the specified player and returns the new value." }, - GetDisplayName = { Params = "", Return = "string", Notes = "Returns the display name of the objective. This name will be shown to the connected players." }, - GetName = { Params = "", Return = "string", Notes = "Returns the internal name of the objective." }, - GetScore = { Params = "string", Return = "Score", Notes = "Returns the score of the specified player." }, - GetType = { Params = "", Return = "eType", Notes = "Returns the type of the objective. (i.e what is being tracked)" }, - Reset = { Params = "", Return = "", Notes = "Resets the scores of the tracked players." }, - ResetScore = { Params = "string", Return = "", Notes = "Reset the score of the specified player." }, - SetDisplayName = { Params = "string", Return = "", Notes = "Sets the display name of the objective." }, - SetScore = { Params = "string, Score", Return = "", Notes = "Sets the score of the specified player." }, - SubScore = { Params = "string, number", Return = "Score", Notes = "Subtracts a value from the score of the specified player and returns the new value." }, - }, - Constants = - { - otAchievement = { Notes = "" }, - otDeathCount = { Notes = "" }, - otDummy = { Notes = "" }, - otHealth = { Notes = "" }, - otPlayerKillCount = { Notes = "" }, - otStat = { Notes = "" }, - otStatBlockMine = { Notes = "" }, - otStatEntityKill = { Notes = "" }, - otStatEntityKilledBy = { Notes = "" }, - otStatItemBreak = { Notes = "" }, - otStatItemCraft = { Notes = "" }, - otStatItemUse = { Notes = "" }, - otTotalKillCount = { Notes = "" }, - }, - }, -- cObjective - - cPainting = - { - Desc = "This class represents a painting in the world. These paintings are special and different from Vanilla in that they can be critical-hit.", - Functions = - { - GetDirection = { Params = "", Return = "number", Notes = "Returns the direction the painting faces. Directions: ZP - 0, ZM - 2, XM - 1, XP - 3. Note that these are not the BLOCK_FACE constants." }, - GetName = { Params = "", Return = "string", Notes = "Returns the name of the painting" }, - }, - - }, -- cPainting - cItemGrid = { Desc = [[This class represents a 2D array of items. It is used as the underlying storage and API for all cases that use a grid of items: @@ -1556,6 +1509,65 @@ end }, }, -- cItems + cJson = + { + Desc = [[ + Exposes the Json parser and serializer available in the server. Plugins can parse Json strings into + Lua tables, and serialize Lua tables into Json strings easily. + ]], + Functions = + { + Parse = { Params = "string", Return = "table", Notes = "Parses the Json in the input string into a Lua table. Returns nil and detailed error message if parsing fails." }, + Serialize = { Params = "table, [options]", Return = "string", Notes = "Serializes the input table into a Json string. The options table, if present, is used to adjust the formatting of the serialized string, see below for details." }, + }, + AdditionalInfo = + { + { + Header = "Serializer options", + Contents = [[ + The "options" parameter given to the cJson:Serialize() function is a dictionary-table of "option + name" -> "option value". The serializer warns if any unknown options are used; the following + options are recognized:

+ + ]], + }, + { + Header = "Code example: Parsing a Json string", + Contents = [==[ + The following code, adapted from the Debuggers plugin, parses a simple Json string and verifies + the results: +
+local t1 = cJson:Parse([[{"a": 1, "b": "2", "c": [3, "4", 5]}]])
+assert(t1.a == 1)
+assert(t1.b == "2")
+assert(t1.c[1] == 3)
+assert(t1.c[2] == "4")
+assert(t1.c[3] == 5)
+
+ ]==], + }, + { + Header = "Code example: Serializing into a Json string", + Contents = [==[ + The following code, adapted from the Debuggers plugin, serializes a simple Lua table into a + string, using custom indentation: +
+local s1 = cJson:Serialize({a = 1, b = "2", c = {3, "4", 5}}, {indentation = "  "})
+LOG("Serialization result: " .. (s1 or ""))
+
+ ]==], + }, + }, + }, -- cJson + cLuaWindow = { Desc = [[This class is used by plugins wishing to display a custom window to the player, unrelated to block entities or entities near the player. The window can be of any type and have any contents that the plugin defines. Callbacks for when the player modifies the window contents and when the player closes the window can be set. @@ -1813,6 +1825,52 @@ a_Player:OpenWindow(Window); Inherits = "cPawn", }, -- cMonster + cObjective = + { + Desc = [[ + This class represents a single scoreboard objective. + ]], + Functions = + { + AddScore = { Params = "string, number", Return = "Score", Notes = "Adds a value to the score of the specified player and returns the new value." }, + GetDisplayName = { Params = "", Return = "string", Notes = "Returns the display name of the objective. This name will be shown to the connected players." }, + GetName = { Params = "", Return = "string", Notes = "Returns the internal name of the objective." }, + GetScore = { Params = "string", Return = "Score", Notes = "Returns the score of the specified player." }, + GetType = { Params = "", Return = "eType", Notes = "Returns the type of the objective. (i.e what is being tracked)" }, + Reset = { Params = "", Return = "", Notes = "Resets the scores of the tracked players." }, + ResetScore = { Params = "string", Return = "", Notes = "Reset the score of the specified player." }, + SetDisplayName = { Params = "string", Return = "", Notes = "Sets the display name of the objective." }, + SetScore = { Params = "string, Score", Return = "", Notes = "Sets the score of the specified player." }, + SubScore = { Params = "string, number", Return = "Score", Notes = "Subtracts a value from the score of the specified player and returns the new value." }, + }, + Constants = + { + otAchievement = { Notes = "" }, + otDeathCount = { Notes = "" }, + otDummy = { Notes = "" }, + otHealth = { Notes = "" }, + otPlayerKillCount = { Notes = "" }, + otStat = { Notes = "" }, + otStatBlockMine = { Notes = "" }, + otStatEntityKill = { Notes = "" }, + otStatEntityKilledBy = { Notes = "" }, + otStatItemBreak = { Notes = "" }, + otStatItemCraft = { Notes = "" }, + otStatItemUse = { Notes = "" }, + otTotalKillCount = { Notes = "" }, + }, + }, -- cObjective + + cPainting = + { + Desc = "This class represents a painting in the world. These paintings are special and different from Vanilla in that they can be critical-hit.", + Functions = + { + GetDirection = { Params = "", Return = "number", Notes = "Returns the direction the painting faces. Directions: ZP - 0, ZM - 2, XM - 1, XP - 3. Note that these are not the BLOCK_FACE constants." }, + GetName = { Params = "", Return = "string", Notes = "Returns the name of the painting" }, + }, + }, -- cPainting + cPawn = { Desc = [[cPawn is a controllable pawn object, controlled by either AI or a player. cPawn inherits all functions and members of {{cEntity}} diff --git a/Server/Plugins/Debuggers/Debuggers.lua b/Server/Plugins/Debuggers/Debuggers.lua index 2b80e15c8..0559a4ef8 100644 --- a/Server/Plugins/Debuggers/Debuggers.lua +++ b/Server/Plugins/Debuggers/Debuggers.lua @@ -1921,6 +1921,34 @@ end +function HandleConsoleTestJson(a_Split, a_EntireCmd) + LOG("Testing Json parsing...") + local t1 = cJson:Parse([[{"a": 1, "b": "2", "c": [3, "4", 5] }]]) + assert(t1.a == 1) + assert(t1.b == "2") + assert(t1.c[1] == 3) + assert(t1.c[2] == "4") + assert(t1.c[3] == 5) + + local t2, msg = cJson:Parse([[{"some": invalid, json}]]) + assert(t2 == nil) + assert(type(msg) == "string") + LOG("Error message returned: " .. msg) + + LOG("Json parsing test succeeded") + + LOG("Testing Json serializing...") + local s1 = cJson:Serialize({a = 1, b = "2", c = {3, "4", 5}}, {indentation = " "}) + LOG("Serialization result: " .. (s1 or "")) + LOG("Json serializing test succeeded") + + return true +end + + + + + function HandleConsoleTestTracer(a_Split, a_EntireCmd) -- Check required params: if not(a_Split[7]) then diff --git a/Server/Plugins/Debuggers/Info.lua b/Server/Plugins/Debuggers/Info.lua index 8f5ef27df..f71ee5509 100644 --- a/Server/Plugins/Debuggers/Info.lua +++ b/Server/Plugins/Debuggers/Info.lua @@ -242,11 +242,17 @@ g_PluginInfo = HelpString = "Tests the world scheduling", }, + ["testjson"] = + { + Handler = HandleConsoleTestJson, + HelpString = "Tests the cJson parser and serializer", + }, + ["testtracer"] = { Handler = HandleConsoleTestTracer, HelpString = "Tests the cLineBlockTracer", - } + }, }, -- ConsoleCommands } -- g_PluginInfo diff --git a/src/Bindings/CMakeLists.txt b/src/Bindings/CMakeLists.txt index 673109ffa..a53e82581 100644 --- a/src/Bindings/CMakeLists.txt +++ b/src/Bindings/CMakeLists.txt @@ -8,6 +8,7 @@ SET (SRCS Bindings.cpp DeprecatedBindings.cpp LuaChunkStay.cpp + LuaJson.cpp LuaNameLookup.cpp LuaServerHandle.cpp LuaState.cpp @@ -30,6 +31,7 @@ SET (HDRS DeprecatedBindings.h LuaChunkStay.h LuaFunctions.h + LuaJson.h LuaNameLookup.h LuaServerHandle.h LuaState.h diff --git a/src/Bindings/LuaJson.cpp b/src/Bindings/LuaJson.cpp new file mode 100644 index 000000000..4fa16273c --- /dev/null +++ b/src/Bindings/LuaJson.cpp @@ -0,0 +1,315 @@ + +// LuaJson.cpp + +// Implements the Json exposure bindings to Lua + +#include "Globals.h" +#include +#include "LuaJson.h" +#include "LuaState.h" +#include "tolua++/include/tolua++.h" +#include "json/json.h" + + + + + +// fwd: + +static void PushJsonValue(const Json::Value & a_Value, cLuaState & a_LuaState); +static Json::Value JsonSerializeValue(cLuaState & a_LuaState); + + + + + +/** Pushes the specified Json array as a table on top of the specified Lua state. +Assumes that a_Value is an array. */ +static void PushJsonArray(const Json::Value & a_Value, cLuaState & a_LuaState) +{ + // Create the appropriately-sized Lua table: + lua_createtable(a_LuaState, static_cast(a_Value.size()), 0); + + // Insert each value to the appropriate index (1-based): + int idx = 1; + for (const auto & v: a_Value) + { + // Include Json null values in the array - it will have holes, but indices will stay the same + PushJsonValue(v, a_LuaState); + lua_rawseti(a_LuaState, -2, idx); + idx += 1; + } // for v: a_Value[] +} + + + + + +/** Pushes the specified Json object as a table on top of the specified Lua state. +Assumes that a_Value is an object. */ +static void PushJsonObject(const Json::Value & a_Value, cLuaState & a_LuaState) +{ + // Create the appropriately-sized Lua table: + lua_createtable(a_LuaState, 0, static_cast(a_Value.size())); + + // Json::Value has no means of iterating over children with their names included. + // We need to iterate over names and "find" them in the object again: + auto names = a_Value.getMemberNames(); + for (const auto & n: names) + { + auto v = a_Value[n]; + if (v.isNull()) + { + // Skip null values + continue; + } + + // Set the value in Lua's table: + a_LuaState.Push(n); + PushJsonValue(v, a_LuaState); + lua_rawset(a_LuaState, -3); + } // for itr - a_Value[] +} + + + + + +/** Pushes the specified Json value as an appropriate type on top of the specified Lua state. */ +void PushJsonValue(const Json::Value & a_Value, cLuaState & a_LuaState) +{ + switch (a_Value.type()) + { + case Json::nullValue: + { + a_LuaState.PushNil(); + break; + } + + case Json::intValue: + case Json::uintValue: + case Json::realValue: + { + a_LuaState.Push(static_cast(a_Value.asDouble())); + break; + } + + case Json::booleanValue: + { + a_LuaState.Push(a_Value.asBool()); + break; + } + + case Json::stringValue: + { + a_LuaState.Push(a_Value.asString()); + break; + } + + case Json::arrayValue: + { + PushJsonArray(a_Value, a_LuaState); + break; + } + + case Json::objectValue: + { + PushJsonObject(a_Value, a_LuaState); + break; + } + } // switch (v.type()) +} + + + + + +/** Serializes the Lua table at the top of the specified Lua state's stack into a Json value. +Lets jsoncpp decides whether to serialize into an object or an array. */ +static Json::Value JsonSerializeTable(cLuaState & a_LuaState) +{ + Json::Value res; + lua_pushnil(a_LuaState); + while (lua_next(a_LuaState, -2) != 0) + { + if (lua_type(a_LuaState, -2) == LUA_TNUMBER) + { + int idx; + a_LuaState.GetStackValue(-2, idx); + res[idx - 1] = JsonSerializeValue(a_LuaState); + } + else + { + AString name; + if (a_LuaState.GetStackValue(-2, name)) + { + res[name] = JsonSerializeValue(a_LuaState); + } + } + lua_pop(a_LuaState, 1); + } + return res; +} + + + + + +/** Serializes the Lua value at the top of the specified Lua state into a Json value. */ +static Json::Value JsonSerializeValue(cLuaState & a_LuaState) +{ + switch (lua_type(a_LuaState, -1)) + { + case LUA_TNUMBER: + { + lua_Number v; + a_LuaState.GetStackValue(-1, v); + return Json::Value(v); + } + case LUA_TSTRING: + { + AString v; + a_LuaState.GetStackValue(-1, v); + return Json::Value(v); + } + case LUA_TTABLE: + { + return JsonSerializeTable(a_LuaState); + } + default: + { + LOGD("Attempting to serialize an unhandled Lua value type: %d", lua_type(a_LuaState, -1)); + return Json::Value(Json::nullValue); + } + } +} + + + + + +static int tolua_cJson_Parse(lua_State * a_LuaState) +{ + // Function signature: + // cJson:Parse("string") -> table + + // Check the param types: + cLuaState L(a_LuaState); + if ( + !L.CheckParamUserTable(1, "cJson") || + !L.CheckParamString(2) || + !L.CheckParamEnd(3) + ) + { + return 0; + } + + // Get the input string: + AString input; + if (!L.GetStackValue(2, input)) + { + LOGWARNING("cJson:Parse(): Cannot read input string"); + L.LogStackTrace(); + return 0; + } + + // Parse the string: + Json::Value root; + Json::Reader reader; + if (!reader.parse(input, root, false)) + { + L.PushNil(); + L.Push(Printf("Parsing Json failed: %s", reader.getFormattedErrorMessages().c_str())); + return 2; + } + + // Push the Json value onto Lua stack: + PushJsonValue(root, L); + return 1; +} + + + + + +static int tolua_cJson_Serialize(lua_State * a_LuaState) +{ + // Function signature: + // cJson:Serialize({table}, [{option1 = value1, option2 = value2}]) -> string + + // Check the param types: + cLuaState L(a_LuaState); + if ( + !L.CheckParamUserTable(1, "cJson") || + !L.CheckParamTable(2) || + !L.CheckParamEnd(4) + ) + { + return 0; + } + + // Push the table to the top of the Lua stack, and call the serializing function: + lua_pushvalue(L, 2); + Json::Value root = JsonSerializeValue(L); + lua_pop(L, 1); + + // Create the writer, with all properties (optional param 3) applied to it: + Json::StreamWriterBuilder builder; + if (lua_istable(L, 3)) + { + lua_pushnil(L); + while (lua_next(L, -2) != 0) + { + if (lua_type(L, -2) == LUA_TSTRING) + { + AString propName, propValue; + if (L.GetStackValues(-2, propName, propValue)) + { + builder[propName] = propValue; + } + } + lua_pop(L, 1); + } + // Check for invalid settings: + Json::Value invalid; + if (!builder.validate(&invalid)) + { + LOGINFO("cJson:Serialize(): detected invalid settings:"); + for (const auto & n: invalid.getMemberNames()) + { + LOGINFO(" \"%s\" (\"%s\")", n.c_str(), invalid[n].asCString()); + } + } + } + auto writer(builder.newStreamWriter()); + + // Serialize the string and push it as the return value: + std::stringstream ss; + writer->write(root, &ss); + L.Push(ss.str()); + return 1; +} + + + + + +void cLuaJson::Bind(cLuaState & a_LuaState) +{ + tolua_beginmodule(a_LuaState, nullptr); + + // Create the cJson API class: + tolua_usertype(a_LuaState, "cJson"); + tolua_cclass(a_LuaState, "cJson", "cJson", "", nullptr); + + // Fill in the functions (alpha-sorted): + tolua_beginmodule(a_LuaState, "cJson"); + tolua_function(a_LuaState, "Parse", tolua_cJson_Parse); + tolua_function(a_LuaState, "Serialize", tolua_cJson_Serialize); + tolua_endmodule(a_LuaState); + tolua_endmodule(a_LuaState); +} + + + + diff --git a/src/Bindings/LuaJson.h b/src/Bindings/LuaJson.h new file mode 100644 index 000000000..6ec8110bc --- /dev/null +++ b/src/Bindings/LuaJson.h @@ -0,0 +1,32 @@ + +// LuaJson.h + +// Declares the Json exposure bindings to Lua + + + + + +#pragma once + + + + + +// fwd: +class cLuaState; + + + + + +class cLuaJson +{ +public: + /** Registers the Json library in the specified Lua state. */ + static void Bind(cLuaState & a_LuaState); +}; + + + + diff --git a/src/Bindings/LuaState.cpp b/src/Bindings/LuaState.cpp index 40cb19304..b03dccad0 100644 --- a/src/Bindings/LuaState.cpp +++ b/src/Bindings/LuaState.cpp @@ -16,6 +16,7 @@ extern "C" #include "Bindings.h" #include "ManualBindings.h" #include "DeprecatedBindings.h" +#include "LuaJson.h" #include "../Entities/Entity.h" #include "../BlockEntities/BlockEntity.h" @@ -180,6 +181,7 @@ void cLuaState::RegisterAPILibs(void) tolua_AllToLua_open(m_LuaState); cManualBindings::Bind(m_LuaState); DeprecatedBindings::Bind(m_LuaState); + cLuaJson::Bind(*this); luaopen_lsqlite3(m_LuaState); luaopen_lxp(m_LuaState); } diff --git a/tests/LoadablePieces/Stubs.cpp b/tests/LoadablePieces/Stubs.cpp index ce30b76dd..bd05d5ce7 100644 --- a/tests/LoadablePieces/Stubs.cpp +++ b/tests/LoadablePieces/Stubs.cpp @@ -9,6 +9,7 @@ #include "SelfTests.h" #include "Bindings.h" #include "Bindings/DeprecatedBindings.h" +#include "Bindings/LuaJson.h" #include "Bindings/ManualBindings.h" #include "BlockEntities/BlockEntity.h" #include "Blocks/BlockHandler.h" @@ -49,6 +50,14 @@ void DeprecatedBindings::Bind(lua_State * a_LuaState) +void cLuaJson::Bind(cLuaState & a_LuaState) +{ +} + + + + + int tolua_AllToLua_open(lua_State * a_LuaState) { return 0;