Initial Commit

master
rubenwardy 2016-06-13 21:27:46 +01:00
commit e53e6d9a8a
3 changed files with 370 additions and 0 deletions

80
.gitignore vendored Normal file
View File

@ -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

89
README.md Normal file
View File

@ -0,0 +1,89 @@
# ChatCmdBuilder
Easily create complex chat commands with no regex.
Created by rubenwardy
License: CC0
# 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 :name", function(name, pos)
local player = minetest.get_player_by_name(name)
if player then
player:set_hp(0)
return true, "Killed " .. name
else
return false, "Unable to find " .. name
end
end)
cmd:sub("move :name to :pos:pos", function(name, pos)
local player = minetest.get_player_by_name(name)
if player then
player:setpos(pos)
return true, "Moved " .. name .. " to " .. minetest.pos_to_string(pos)
else
return false, "Unable to find " .. name
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 0,0,0` to teleport a user.
## Introduction to Routing
A route is a string. Let's look at `move :name to :pos:pos`:
* `move` and `to` are constants. They need to be there in order to match.
* `:name` and `:pos:pos` are parameters. They're passed to the function.
* The second `pos` in `:pos:pos` after `:` is the param type. `:name` 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
## 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
```

201
init.lua Normal file
View File

@ -0,0 +1,201 @@
local ChatCmdBuilder = {}
function ChatCmdBuilder.new(name, func, def)
def = def or {}
def.func = ChatCmdBuilder.build(name, func)
minetest.register_chatcommand(name, def)
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()
print("Pattern: " .. sub.pattern)
table.insert(self._subs, sub)
end
func(cmd)
return (function(name, param)
print("Running <" .. name .. "> CMD " .. param)
for i = 1, #cmd._subs do
local sub = cmd._subs[i]
local res = { string.match(param, sub.pattern) }
if res then
local pointer = 1
local params = {}
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)
end
local function run_tests()
if not (ChatCmdBuilder.build(function(cmd)
cmd:sub("bar :one and :two:word", function(one, two)
if 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 :name to :pos:pos", function(name, pos)
if name == "singleplayer" and pos.x == 0 and pos.y == 1 and pos.z == 2 then
return true
end
end)
end)
if not move("singleplayer", "move singleplayer to 0,1,2") then
error("Test 2 failed")
end
if move("singleplayer", "move singleplayer to abc def sdosd") then
error("Test 3 failed")
end
if not (ChatCmdBuilder.build(function(cmd)
cmd:sub("does :one:int plus :two:int equal :three:int", function(one, two, three)
if one + two == three then
return true
end
end)
end))("singleplayer", "does 1 plus 2 equal 3") then
error("Test 4 failed")
end
end
if not minetest then
run_tests()
end