From 3f552c3ef8c0bea5c539956b80dd6b0109ffaa01 Mon Sep 17 00:00:00 2001 From: rubenwardy Date: Tue, 7 Nov 2017 22:50:49 +0000 Subject: [PATCH] Add chatcmdbuilder --- chatcmdbuilder/.gitignore | 80 ++++++++++++ chatcmdbuilder/LICENSE | 19 +++ chatcmdbuilder/README.md | 89 +++++++++++++ chatcmdbuilder/description.txt | 1 + chatcmdbuilder/init.lua | 230 +++++++++++++++++++++++++++++++++ chatcmdbuilder/mod.conf | 7 + 6 files changed, 426 insertions(+) create mode 100644 chatcmdbuilder/.gitignore create mode 100644 chatcmdbuilder/LICENSE create mode 100644 chatcmdbuilder/README.md create mode 100644 chatcmdbuilder/description.txt create mode 100644 chatcmdbuilder/init.lua create mode 100644 chatcmdbuilder/mod.conf diff --git a/chatcmdbuilder/.gitignore b/chatcmdbuilder/.gitignore new file mode 100644 index 0000000..9cac31c --- /dev/null +++ b/chatcmdbuilder/.gitignore @@ -0,0 +1,80 @@ + +# Created by https://www.gitignore.io/api/lin,linux,windows,lua + +#!! ERROR: lin is undefined. Use list command to see defined gitignore types !!# + +### Linux ### +*~ + +# temporary files which can be created if a process still has a handle open of a deleted file +.fuse_hidden* + +# KDE directory preferences +.directory + +# Linux trash folder which might appear on any partition or disk +.Trash-* + + +### Windows ### +# Windows image file caches +Thumbs.db +ehthumbs.db + +# Folder config file +Desktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msm +*.msp + +# Windows shortcuts +*.lnk + + +### Lua ### +# Compiled Lua sources +luac.out + +# luarocks build files +*.src.rock +*.zip +*.tar.gz + +# Object files +*.o +*.os +*.ko +*.obj +*.elf + +# Precompiled Headers +*.gch +*.pch + +# Libraries +*.lib +*.a +*.la +*.lo +*.def +*.exp + +# Shared objects (inc. Windows DLLs) +*.dll +*.so +*.so.* +*.dylib + +# Executables +*.exe +*.out +*.app +*.i*86 +*.x86_64 +*.hex diff --git a/chatcmdbuilder/LICENSE b/chatcmdbuilder/LICENSE new file mode 100644 index 0000000..588da15 --- /dev/null +++ b/chatcmdbuilder/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2016-17 rubenwardy + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/chatcmdbuilder/README.md b/chatcmdbuilder/README.md new file mode 100644 index 0000000..7d52585 --- /dev/null +++ b/chatcmdbuilder/README.md @@ -0,0 +1,89 @@ +# ChatCmdBuilder + +Easily create complex chat commands with no regex. +Created by rubenwardy +License: MIT + +# Usage + +## Registering Chat Commands + +`ChatCmdBuilder.new(name, setup)` registers a new chat command called `name`. +Setup is called immediately after calling `new` to initialise subcommands. + +You can set values in the chat command definition by using def: +`ChatCmdBuilder.new(name, setup, def)`. + +Here is an example: + +```Lua +ChatCmdBuilder.new("admin", function(cmd) + cmd:sub("kill :target", function(name, target) + local player = minetest.get_player_by_name(target) + if player then + player:set_hp(0) + return true, "Killed " .. target + else + return false, "Unable to find " .. target + end + end) + + cmd:sub("move :target to :pos:pos", function(name, target, pos) + local player = minetest.get_player_by_name(target) + if player then + player:setpos(pos) + return true, "Moved " .. target .. " to " .. minetest.pos_to_string(pos) + else + return false, "Unable to find " .. target + end + end) +end, { + description = "Admin tools", + privs = { + kick = true, + ban = true + } +}) +``` + +A player could then do `/admin kill player1` to kill player1, +or `/admin move player1 to 0,0,0` to teleport a user. + +## Introduction to Routing + +A route is a string. Let's look at `move :target to :pos:pos`: + +* `move` and `to` are constants. They need to be there in order to match. +* `:target` and `:pos:pos` are parameters. They're passed to the function. +* The second `pos` in `:pos:pos` after `:` is the param type. `:target` has an implicit + type of `word`. + +## Param Types + +* `word` - default. Any string without spaces. +* `number` - Any number, including decimals +* `int` - Any integer, no decimals +* `text` - Any string +* `pos` - 1,2,3 or 1.1,2,3.4567 or (1,2,3) or 1.2, 2 ,3.2 + +## Build chat command function + +If you don't want to register the chatcommand at this point, you can just generate +a function using `ChatCmdBuilder.build`. + +For example, this is the full definition of ChatCmdBuilder.new: + +```Lua +function ChatCmdBuilder.new(name, func, def) + def = def or {} + def.func = ChatCmdBuilder.build(name, func) + minetest.register_chatcommand(name, def) +end +``` + +## Run tests + +```Bash +sudo apt-get install luajit +luajit init.lua +``` diff --git a/chatcmdbuilder/description.txt b/chatcmdbuilder/description.txt new file mode 100644 index 0000000..6e5d637 --- /dev/null +++ b/chatcmdbuilder/description.txt @@ -0,0 +1 @@ +A library to make registering chat commands easier diff --git a/chatcmdbuilder/init.lua b/chatcmdbuilder/init.lua new file mode 100644 index 0000000..400d250 --- /dev/null +++ b/chatcmdbuilder/init.lua @@ -0,0 +1,230 @@ +ChatCmdBuilder = {} + +function ChatCmdBuilder.new(name, func, def) + def = def or {} + local cmd = ChatCmdBuilder.build(func) + cmd.def = def + def.func = cmd.run + minetest.register_chatcommand(name, def) + return cmd +end + +local STATE_READY = 1 +local STATE_PARAM = 2 +local STATE_PARAM_TYPE = 3 +local bad_chars = {} +bad_chars["("] = true +bad_chars[")"] = true +bad_chars["."] = true +bad_chars["%"] = true +bad_chars["+"] = true +bad_chars["-"] = true +bad_chars["*"] = true +bad_chars["?"] = true +bad_chars["["] = true +bad_chars["^"] = true +bad_chars["$"] = true +local function escape(char) + if bad_chars[char] then + return "%" .. char + else + return char + end +end + +function ChatCmdBuilder.build(func) + local cmd = { + _subs = {} + } + function cmd:sub(route, func, def) + print("Parsing " .. route) + + def = def or {} + if string.trim then + route = string.trim(route) + end + + local sub = { + pattern = "^", + params = {}, + func = func + } + + -- End of param reached: add it to the pattern + local param = "" + local param_type = "" + local should_be_eos = false + local function finishParam() + if param ~= "" and param_type ~= "" then + print(" - Found param " .. param .. " type " .. param_type) + + if param_type == "pos" then + sub.pattern = sub.pattern .. "%(? *(%-?[%d.]+) *, *(%-?[%d.]+) *, *(%-?[%d.]+) *%)?" + elseif param_type == "text" then + sub.pattern = sub.pattern .. "(*+)" + should_be_eos = true + elseif param_type == "number" then + sub.pattern = sub.pattern .. "([%d.]+)" + elseif param_type == "int" then + sub.pattern = sub.pattern .. "([%d]+)" + else + if param_type ~= "word" then + print("Unrecognised param_type=" .. param_type .. ", using 'word' type instead") + param_type = "word" + end + sub.pattern = sub.pattern .. "([^ ]+)" + end + + table.insert(sub.params, param_type) + + param = "" + param_type = "" + end + end + + -- Iterate through the route to find params + local state = STATE_READY + for i = 1, #route do + local c = route:sub(i, i) + if should_be_eos then + error("Should be end of string. Nothing is allowed after a param of type text.") + end + + if state == STATE_READY then + if c == ":" then + print(" - Found :, entering param") + state = STATE_PARAM + param_type = "word" + else + sub.pattern = sub.pattern .. escape(c) + end + elseif state == STATE_PARAM then + if c == ":" then + print(" - Found :, entering param type") + state = STATE_PARAM_TYPE + param_type = "" + elseif c:match("%W") then + print(" - Found nonalphanum, leaving param") + state = STATE_READY + finishParam() + sub.pattern = sub.pattern .. escape(c) + else + param = param .. c + end + elseif state == STATE_PARAM_TYPE then + if c:match("%W") then + print(" - Found nonalphanum, leaving param type") + state = STATE_READY + finishParam() + sub.pattern = sub.pattern .. escape(c) + else + param_type = param_type .. c + end + end + end + print(" - End of route") + finishParam() + sub.pattern = sub.pattern .. "$" + print("Pattern: " .. sub.pattern) + + table.insert(self._subs, sub) + end + + if func then + func(cmd) + end + + cmd.run = function(name, param) + for i = 1, #cmd._subs do + local sub = cmd._subs[i] + local res = { string.match(param, sub.pattern) } + if #res > 0 then + local pointer = 1 + local params = { name } + for j = 1, #sub.params do + local param = sub.params[j] + if param == "pos" then + local pos = { + x = tonumber(res[pointer]), + y = tonumber(res[pointer + 1]), + z = tonumber(res[pointer + 2]) + } + table.insert(params, pos) + pointer = pointer + 3 + elseif param == "number" or param == "int" then + table.insert(params, tonumber(res[pointer])) + pointer = pointer + 1 + else + table.insert(params, res[pointer]) + pointer = pointer + 1 + end + end + return sub.func(unpack(params)) + end + end + print("No matches") + end + + return cmd +end + +local function run_tests() + if not (ChatCmdBuilder.build(function(cmd) + cmd:sub("bar :one and :two:word", function(name, one, two) + if name == "singleplayer" and one == "abc" and two == "def" then + return true + end + end) + end))("singleplayer", "bar abc and def") then + error("Test 1 failed") + end + + local move = ChatCmdBuilder.build(function(cmd) + cmd:sub("move :target to :pos:pos", function(name, target, pos) + if name == "singleplayer" and target == "player1" and + pos.x == 0 and pos.y == 1 and pos.z == 2 then + return true + end + end) + end) + if not move("singleplayer", "move player1 to 0,1,2") then + error("Test 2 failed") + end + if not move("singleplayer", "move player1 to (0,1,2)") then + error("Test 3 failed") + end + if not move("singleplayer", "move player1 to 0, 1,2") then + error("Test 4 failed") + end + if not move("singleplayer", "move player1 to 0 ,1, 2") then + error("Test 5 failed") + end + if not move("singleplayer", "move player1 to 0, 1, 2") then + error("Test 6 failed") + end + if not move("singleplayer", "move player1 to 0 ,1 ,2") then + error("Test 7 failed") + end + if not move("singleplayer", "move player1 to ( 0 ,1 ,2)") then + error("Test 7 failed") + end + if move("singleplayer", "move player1 to abc,def,sdosd") then + error("Test 8 failed") + end + if move("singleplayer", "move player1 to abc def sdosd") then + error("Test 8 failed") + end + + if not (ChatCmdBuilder.build(function(cmd) + cmd:sub("does :one:int plus :two:int equal :three:int", function(name, one, two, three) + if name == "singleplayer" and one + two == three then + return true + end + end) + end))("singleplayer", "does 1 plus 2 equal 3") then + error("Test 9 failed") + end +end +if not minetest then + run_tests() +end diff --git a/chatcmdbuilder/mod.conf b/chatcmdbuilder/mod.conf new file mode 100644 index 0000000..1172971 --- /dev/null +++ b/chatcmdbuilder/mod.conf @@ -0,0 +1,7 @@ +name = lib_chatcmdbuilder +title = Chat Command Builder +author = rubenwardy +description = A library to make registering chat commands easier +license = MIT +forum = https://forum.minetest.net/viewtopic.php?t=14899 +version = 0.1.0