diff --git a/res/layouts/console.xml b/res/layouts/console.xml
new file mode 100644
index 00000000..bbe869d0
--- /dev/null
+++ b/res/layouts/console.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
diff --git a/res/layouts/console.xml.lua b/res/layouts/console.xml.lua
new file mode 100644
index 00000000..7ed29c1f
--- /dev/null
+++ b/res/layouts/console.xml.lua
@@ -0,0 +1,53 @@
+history = session.get_entry("commands_history")
+history_pointer = #history
+
+function setup_variables()
+ local x,y,z = player.get_pos(hud.get_player())
+ console.set('pos.x', x)
+ console.set('pos.y', y)
+ console.set('pos.z', z)
+end
+
+function on_history_up()
+ if history_pointer == 0 then
+ return
+ end
+ document.prompt.text = history[history_pointer]
+ document.prompt.caret = -1
+ history_pointer = history_pointer - 1
+end
+
+function on_history_down()
+ if history_pointer == #history-1 then
+ return
+ end
+ history_pointer = history_pointer + 1
+ document.prompt.text = history[history_pointer + 1]
+ document.prompt.caret = -1
+end
+
+function add_to_history(text)
+ table.insert(history, text)
+ history_pointer = #history
+end
+
+function submit(text)
+ add_to_history(text)
+ setup_variables()
+
+ local status, result = pcall(function() return console.execute(text) end)
+ if result ~= nil then
+ local prevtext = document.log.text
+ if #prevtext == 0 then
+ document.log:paste(tostring(result))
+ else
+ document.log:paste('\n'..tostring(result))
+ end
+ end
+ document.prompt.text = ""
+ document.prompt.focused = true
+end
+
+function on_open()
+ document.prompt.focused = true
+end
diff --git a/res/scripts/stdcmd.lua b/res/scripts/stdcmd.lua
new file mode 100644
index 00000000..48741102
--- /dev/null
+++ b/res/scripts/stdcmd.lua
@@ -0,0 +1,89 @@
+local SEPARATOR = "________________"
+SEPARATOR = SEPARATOR..SEPARATOR..SEPARATOR
+
+function build_scheme(command)
+ local str = command.name.." "
+ for i,arg in ipairs(command.args) do
+ if arg.optional then
+ str = str.."["..arg.name.."] "
+ else
+ str = str.."<"..arg.name.."> "
+ end
+ end
+ return str
+end
+
+console.add_command(
+ "help name:str=''",
+ "Show help infomation for the specified command",
+ function (args, kwargs)
+ local name = args[1]
+ if #name == 0 then
+ local commands = console.get_commands_list()
+ table.sort(commands)
+ local str = "Available commands:"
+ for i,k in ipairs(commands) do
+ str = str.."\n "..build_scheme(console.get_command_info(k))
+ end
+ return str.."\nuse 'help '"
+ end
+ local command = console.get_command_info(name)
+ if command == nil then
+ return string.format("command %q not found", name)
+ end
+ local where = ""
+ local str = SEPARATOR.."\n"..command.description.."\n"..name.." "
+ for i,arg in ipairs(command.args) do
+ where = where.."\n "..arg.name.." - "..arg.type
+ if arg.optional then
+ str = str.."["..arg.name.."] "
+ where = where.." (optional)"
+ else
+ str = str.."<"..arg.name.."> "
+ end
+ end
+ if #command.args then
+ str = str.."\nwhere"..where
+ end
+
+ return str.."\n"..SEPARATOR
+ end
+)
+
+console.add_command(
+ "obj.tp obj:sel=$obj.id x:num~pos.x y:num~pos.y z:num~pos.z",
+ "Teleport object",
+ function (args, kwargs)
+ player.set_pos(unpack(args))
+ end
+)
+console.add_command(
+ "echo value:str",
+ "Print value to the console",
+ function (args, kwargs)
+ return args[1]
+ end
+)
+console.add_command(
+ "time.set value:num",
+ "Set day time [0..1] where 0 is midnight, 0.5 is noon",
+ function (args, kwargs)
+ return world.set_day_time(args[1])
+ end
+)
+console.add_command(
+ "blocks.fill id:str x:num~pos.x y:num~pos.y z:num~pos.z w:int h:int d:int",
+ "Fill specified zone with blocks",
+ function (args, kwargs)
+ local name, x, y, z, w, h, d = unpack(args)
+ local id = block.index(name)
+ for ly=0,h-1 do
+ for lz=0,d-1 do
+ for lx=0,w-1 do
+ block.set(x+lx, y+ly, z+lz, id)
+ end
+ end
+ end
+ return tostring(w*h*d).." blocks set"
+ end
+)
diff --git a/res/texts/en_US.txt b/res/texts/en_US.txt
index 94d6774b..979c7330 100644
--- a/res/texts/en_US.txt
+++ b/res/texts/en_US.txt
@@ -9,6 +9,7 @@ world.generators.default=Default
world.generators.flat=Flat
# Bindings
+devtools.console=Console
movement.forward=Forward
movement.back=Back
movement.left=Left
diff --git a/res/texts/ru_RU.txt b/res/texts/ru_RU.txt
index 04ee4e69..ec2946ef 100644
--- a/res/texts/ru_RU.txt
+++ b/res/texts/ru_RU.txt
@@ -56,6 +56,7 @@ settings.UI Sounds=Звуки Интерфейса
settings.V-Sync=Вертикальная Синхронизация
# Управление
+devtools.console=Консоль
movement.forward=Вперёд
movement.back=Назад
movement.left=Влево
diff --git a/src/coders/commons.cpp b/src/coders/commons.cpp
index 50538a50..df59fcb3 100644
--- a/src/coders/commons.cpp
+++ b/src/coders/commons.cpp
@@ -154,8 +154,13 @@ void BasicParser::expectNewLine() {
}
}
-void BasicParser::goBack() {
- if (pos) pos--;
+void BasicParser::goBack(size_t count) {
+ if (pos < count) {
+ throw std::runtime_error("pos < jump");
+ }
+ if (pos) {
+ pos -= count;
+ }
}
char BasicParser::peek() {
@@ -166,6 +171,13 @@ char BasicParser::peek() {
return source[pos];
}
+char BasicParser::peekNoJump() {
+ if (pos >= source.length()) {
+ throw error("unexpected end");
+ }
+ return source[pos];
+}
+
std::string_view BasicParser::readUntil(char c) {
int start = pos;
while (hasNext() && source[pos] != c) {
diff --git a/src/coders/commons.hpp b/src/coders/commons.hpp
index c45d4f28..1584c92d 100644
--- a/src/coders/commons.hpp
+++ b/src/coders/commons.hpp
@@ -31,11 +31,11 @@ inline bool is_whitespace(int c) {
}
inline bool is_identifier_start(int c) {
- return (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || c == '_' || c == '-' || c == '.';
+ return (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || c == '_' || c == '.';
}
inline bool is_identifier_part(int c) {
- return is_identifier_start(c) || is_digit(c);
+ return is_identifier_start(c) || is_digit(c) || c == '-';
}
inline int hexchar2int(int c) {
@@ -86,7 +86,7 @@ protected:
void expect(const std::string& substring);
bool isNext(const std::string& substring);
void expectNewLine();
- void goBack();
+ void goBack(size_t count=1);
int64_t parseSimpleInt(int base);
dynamic::Value parseNumber(int sign);
@@ -99,6 +99,7 @@ public:
std::string parseName();
bool hasNext();
char peek();
+ char peekNoJump();
char nextChar();
BasicParser(std::string_view file, std::string_view source);
diff --git a/src/coders/json.cpp b/src/coders/json.cpp
index f6a7a19e..89f06354 100644
--- a/src/coders/json.cpp
+++ b/src/coders/json.cpp
@@ -36,14 +36,6 @@ inline void newline(
}
}
-void stringify(
- const Value& value,
- std::stringstream& ss,
- int indent,
- const std::string& indentstr,
- bool nice
-);
-
void stringifyObj(
const Map* obj,
std::stringstream& ss,
@@ -91,6 +83,8 @@ void stringifyValue(
ss << *num;
} else if (auto str = std::get_if(&value)) {
ss << util::escape(*str);
+ } else {
+ ss << "null";
}
}
diff --git a/src/core_defs.cpp b/src/core_defs.cpp
index cb6eb9c4..12e840b4 100644
--- a/src/core_defs.cpp
+++ b/src/core_defs.cpp
@@ -24,6 +24,7 @@ void corecontent::setup(ContentBuilder* builder) {
}
void corecontent::setup_bindings() {
+ Events::bind(BIND_DEVTOOLS_CONSOLE, inputtype::keyboard, keycode::GRAVE_ACCENT);
Events::bind(BIND_MOVE_FORWARD, inputtype::keyboard, keycode::W);
Events::bind(BIND_MOVE_BACK, inputtype::keyboard, keycode::S);
Events::bind(BIND_MOVE_RIGHT, inputtype::keyboard, keycode::D);
diff --git a/src/core_defs.hpp b/src/core_defs.hpp
index f071dd1d..1a2b0a9f 100644
--- a/src/core_defs.hpp
+++ b/src/core_defs.hpp
@@ -9,6 +9,7 @@ inline const std::string CORE_AIR = "core:air";
inline const std::string TEXTURE_NOTFOUND = "notfound";
// built-in bindings
+inline const std::string BIND_DEVTOOLS_CONSOLE = "devtools.console";
inline const std::string BIND_MOVE_FORWARD = "movement.forward";
inline const std::string BIND_MOVE_BACK = "movement.back";
inline const std::string BIND_MOVE_LEFT = "movement.left";
diff --git a/src/data/dynamic.cpp b/src/data/dynamic.cpp
index cbfe6a7b..4d60d2a5 100644
--- a/src/data/dynamic.cpp
+++ b/src/data/dynamic.cpp
@@ -242,6 +242,20 @@ size_t Map::size() const {
return values.size();
}
+static const std::string TYPE_NAMES[] {
+ "none",
+ "map",
+ "list",
+ "string",
+ "number",
+ "bool",
+ "integer",
+};
+
+const std::string& dynamic::type_name(const Value& value) {
+ return TYPE_NAMES[value.index()];
+}
+
List_sptr dynamic::create_list(std::initializer_list values) {
return std::make_shared(values);
}
@@ -249,3 +263,19 @@ List_sptr dynamic::create_list(std::initializer_list values) {
Map_sptr dynamic::create_map(std::initializer_list> entries) {
return std::make_shared