mirror of
https://github.com/sbrl/Minetest-WorldEditAdditions.git
synced 2024-11-22 15:33:00 +00:00
Merge pull request #80 from sbrl/VorTechnix
Implement new axis keyword parser system
This commit is contained in:
commit
be0612e6be
5 changed files with 481 additions and 2 deletions
158
.tests/parse/axes/axes_parser.test.lua
Normal file
158
.tests/parse/axes/axes_parser.test.lua
Normal file
|
@ -0,0 +1,158 @@
|
|||
local Vector3 = require("worldeditadditions.utils.vector3")
|
||||
|
||||
local facing_dirs = dofile("./.tests/parse/axes/include_facing_dirs.lua")
|
||||
|
||||
local parse = require("worldeditadditions.utils.parse.axes_parser")
|
||||
local parse_axes = parse.keytable
|
||||
|
||||
|
||||
describe("parse_axes", function()
|
||||
|
||||
-- Basic tests
|
||||
it("should work on single horizontal axes", function()
|
||||
local minv, maxv = parse_axes({
|
||||
"x", "3",
|
||||
"-z", "10",
|
||||
}, facing_dirs.x_pos)
|
||||
assert.is.truthy(minv)
|
||||
assert.are.same(Vector3.new(0, 0, -10), minv)
|
||||
assert.are.same(Vector3.new(3, 0, 0), maxv)
|
||||
end)
|
||||
|
||||
it("should handle axis clumps and orphan (universal) values", function()
|
||||
local minv, maxv = parse_axes({
|
||||
"xz", "-3",
|
||||
"10",
|
||||
}, facing_dirs.x_pos)
|
||||
assert.is.truthy(minv)
|
||||
assert.are.same(Vector3.new(-3, 0, -3), minv)
|
||||
assert.are.same(Vector3.new(10, 10, 10), maxv)
|
||||
end)
|
||||
|
||||
it("should work on directions and their abriviations", function()
|
||||
local minv, maxv = parse_axes({
|
||||
"l", "3", -- +z
|
||||
"-r", "-3", -- +z
|
||||
"b", "-10", -- -x
|
||||
}, facing_dirs.x_pos)
|
||||
assert.is.truthy(minv)
|
||||
assert.are.same(Vector3.new(0, 0, -3), minv)
|
||||
assert.are.same(Vector3.new(10, 0, 3), maxv)
|
||||
end)
|
||||
|
||||
it("should work with compass directions and their abriviations", function()
|
||||
local minv, maxv = parse_axes({
|
||||
"n", "3", -- +z
|
||||
"south", "3", -- -z
|
||||
"-west", "10", -- +x
|
||||
}, facing_dirs.x_pos)
|
||||
assert.is.truthy(minv)
|
||||
assert.are.same(Vector3.new(0, 0, -3), minv)
|
||||
assert.are.same(Vector3.new(10, 0, 3), maxv)
|
||||
end)
|
||||
|
||||
it("should work with ?", function()
|
||||
local minv, maxv = parse_axes({
|
||||
"?", "3", -- -z
|
||||
}, facing_dirs.z_neg)
|
||||
assert.is.truthy(minv)
|
||||
assert.are.same(Vector3.new(0, 0, -3), minv)
|
||||
assert.are.same(Vector3.new(0, 0, 0), maxv)
|
||||
end)
|
||||
|
||||
it("should work with complex relative / absolute combinations", function()
|
||||
local minv, maxv = parse_axes({
|
||||
"f", "3", -- +x
|
||||
"left", "10", -- +z
|
||||
"y", "77",
|
||||
"x", "30",
|
||||
"back", "99",
|
||||
}, facing_dirs.x_pos)
|
||||
assert.is.truthy(minv)
|
||||
assert.are.same(Vector3.new(-99, 0, 0), minv)
|
||||
assert.are.same(Vector3.new(33, 77, 10), maxv)
|
||||
end)
|
||||
|
||||
it("should work with complex relative / absolute combinations with negative facing_dirs", function()
|
||||
local minv, maxv = parse_axes({
|
||||
"f", "3", -- -z
|
||||
"l", "10", -- +x
|
||||
"y", "77",
|
||||
"x", "30",
|
||||
"b", "99", -- +z
|
||||
}, facing_dirs.z_neg)
|
||||
assert.is.truthy(minv)
|
||||
assert.are.same(Vector3.new(0, 0, -3), minv)
|
||||
assert.are.same(Vector3.new(40, 77, 99), maxv)
|
||||
end)
|
||||
|
||||
it("should return 2 0,0,0 vectors if no input", function()
|
||||
local minv, maxv = parse_axes({
|
||||
-- No input
|
||||
}, facing_dirs.z_neg)
|
||||
assert.is.truthy(minv)
|
||||
assert.are.same(Vector3.new(0, 0, 0), minv)
|
||||
assert.are.same(Vector3.new(0, 0, 0), maxv)
|
||||
end)
|
||||
|
||||
-- Options tests
|
||||
it("should mirror the max values of the two vectors if mirroring keyword is present", function()
|
||||
local minv, maxv = parse_axes({
|
||||
"x", "3",
|
||||
"f", "-5", -- +x
|
||||
"z", "-10",
|
||||
"mir",
|
||||
}, facing_dirs.x_pos)
|
||||
assert.is.truthy(minv)
|
||||
assert.are.same(Vector3.new(-5, 0, -10), minv)
|
||||
assert.are.same(Vector3.new(5, 0, 10), maxv)
|
||||
end)
|
||||
|
||||
it("should return a single vector if 'sum' input is truthy", function()
|
||||
local minv, maxv = parse_axes({
|
||||
"x", "3",
|
||||
"z", "-10",
|
||||
}, facing_dirs.x_pos,"sum")
|
||||
assert.is.truthy(minv)
|
||||
assert.are.same(Vector3.new(3, 0, -10), minv)
|
||||
assert.are.same(nil, maxv)
|
||||
end)
|
||||
|
||||
it("should dissable mirroring if 'sum' input is truthy", function()
|
||||
local minv, maxv = parse_axes({
|
||||
"x", "3",
|
||||
"f", "-5", -- +x
|
||||
"z", "-10",
|
||||
"sym",
|
||||
}, facing_dirs.x_pos,"sum")
|
||||
assert.is.truthy(minv)
|
||||
assert.are.same(Vector3.new(-2, 0, -10), minv)
|
||||
assert.are.same(nil, maxv)
|
||||
end)
|
||||
|
||||
-- Error tests
|
||||
it("should return error if bad axis/dir", function()
|
||||
local minv, maxv = parse_axes({
|
||||
"f", "3", -- +x
|
||||
"lift", "10", -- Invalid axis
|
||||
"y", "77",
|
||||
"x", "30",
|
||||
"back", "99", -- -x
|
||||
}, facing_dirs.x_pos)
|
||||
assert.are.same(false, minv)
|
||||
assert.are.same("string", type(maxv))
|
||||
end)
|
||||
|
||||
it("should return error if bad value", function()
|
||||
local minv, maxv = parse_axes({
|
||||
"f", "3", -- +x
|
||||
"left", "10", -- +z
|
||||
"y", "!Q", -- Invalid value
|
||||
"x", "30",
|
||||
"back", "99",
|
||||
}, facing_dirs.x_pos)
|
||||
assert.are.same(false, minv)
|
||||
assert.are.same("string", type(maxv))
|
||||
end)
|
||||
|
||||
end)
|
|
@ -71,7 +71,7 @@ end
|
|||
-- @param str string String to check (be sure to remove any + or -).
|
||||
-- @return bool If string is a valid dir then true.
|
||||
function selection.check_dir(str)
|
||||
return (str == "front" or str == "back" or str == "left" or str == "right" or str == "up" or str == "down")
|
||||
return (str == "facing" or str == "front" or str == "back" or str == "left" or str == "right" or str == "up" or str == "down")
|
||||
end
|
||||
|
||||
return selection
|
||||
|
|
224
worldeditadditions/utils/parse/axes_parser.lua
Normal file
224
worldeditadditions/utils/parse/axes_parser.lua
Normal file
|
@ -0,0 +1,224 @@
|
|||
-- ██████ █████ ██████ ███████ ███████ █████ ██ ██ ███████ ███████
|
||||
-- ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██
|
||||
-- ██████ ███████ ██████ ███████ █████ ███████ ███ █████ ███████
|
||||
-- ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██
|
||||
-- ██ ██ ██ ██ ██ ███████ ███████ ██ ██ ██ ██ ███████ ███████
|
||||
|
||||
-- Error codes: https://kinsta.com/blog/http-status-codes/
|
||||
|
||||
--- FOR TESTING ---
|
||||
local function unpack(tbl)
|
||||
if table.unpack then
|
||||
return table.unpack(tbl)
|
||||
else return unpack(tbl) end
|
||||
end
|
||||
---------------
|
||||
|
||||
local Vector3
|
||||
if worldeditadditions then
|
||||
local wea = worldeditadditions
|
||||
Vector3 = dofile(wea.modpath.."/utils/vector3.lua")
|
||||
else
|
||||
Vector3 = require("worldeditadditions.utils.vector3")
|
||||
end
|
||||
|
||||
local key_instance
|
||||
if worldeditadditions then
|
||||
local wea = worldeditadditions
|
||||
key_instance = dofile(wea.modpath.."/utils/parse/key_instance.lua")
|
||||
else
|
||||
key_instance = require("worldeditadditions.utils.parse.key_instance")
|
||||
end
|
||||
|
||||
--- Unified Axis Keywords banks
|
||||
local keywords = {
|
||||
-- Compass keywords
|
||||
compass = {
|
||||
n = "z", north = "z",
|
||||
["-n"] = "-z", ["-north"] = "-z",
|
||||
s = "-z", south = "-z",
|
||||
["-s"] = "z", ["-south"] = "z",
|
||||
e = "x", east = "x",
|
||||
["-e"] = "-x", ["-east"] = "-x",
|
||||
w = "-x", west = "-x",
|
||||
["-w"] = "x", ["-west"] = "x",
|
||||
},
|
||||
|
||||
-- Direction keywords
|
||||
dir = {
|
||||
["?"] = "front", f = "front",
|
||||
facing = "front", front = "front",
|
||||
b = "back", back = "back",
|
||||
behind = "back", rear = "back",
|
||||
l = "left", left = "left",
|
||||
r = "right", right = "right",
|
||||
u = "up", up = "up",
|
||||
d = "down", down = "down",
|
||||
},
|
||||
|
||||
-- Mirroring keywords
|
||||
mirroring = {
|
||||
sym = true, symmetrical = true,
|
||||
mirror = true, mir = true,
|
||||
rev = true, reverse = true,
|
||||
["true"] = true
|
||||
},
|
||||
}
|
||||
|
||||
--- Initialize parser function container
|
||||
local parse = {}
|
||||
|
||||
--- Processes an axis declaration into ordered xyz format. (Supports axis clumping)
|
||||
-- For example, "zzy" would become "yz"
|
||||
-- @param: str: String: Axis declaration to parse
|
||||
-- @returns: Table|Bool: axis | axes | false
|
||||
function parse.axis(str)
|
||||
local axes, ret = {"x","y","z"}, {}
|
||||
for i,v in ipairs(axes) do
|
||||
if str:match(v) then table.insert(ret,v) end
|
||||
end
|
||||
if #ret > 0 and str:match("^[xyz]+$") then
|
||||
return ret
|
||||
elseif str == "h" then
|
||||
return {"x", "z"}
|
||||
elseif str == "v" then
|
||||
return {"y"}
|
||||
else return false end
|
||||
end
|
||||
|
||||
--- Processes an number from a string.
|
||||
-- @param: str: String: string to parse
|
||||
-- @returns: Number|Bool: processed number | false
|
||||
function parse.num(str)
|
||||
str = tostring(str) -- To prevent meltdown if str isn't a string
|
||||
local num = str:match("^-?%d+%.?%d*$")
|
||||
if num then
|
||||
return tonumber(num)
|
||||
else return false end
|
||||
end
|
||||
|
||||
--- Checks if a string is a valid Unified Axis Keyword. (Supports axis clumping)
|
||||
-- @param: str: String: Keyword instance to parse
|
||||
-- @returns: Key Instance: returns keyword type, processed keyword content and signed number (or nil)
|
||||
function parse.keyword(str)
|
||||
if type(str) ~= "string" then
|
||||
return key_instance.new("err", "Error: \""..tostring(str).."\" is not a string.", 404)
|
||||
elseif keywords.compass[str] then
|
||||
str = keywords.compass[str]
|
||||
end
|
||||
local sign = 1
|
||||
if str:sub(1,1) == "-" then
|
||||
sign = -1
|
||||
str = str:sub(2)
|
||||
end
|
||||
|
||||
local axes = parse.axis(str)
|
||||
if axes then
|
||||
return key_instance.new("axis", axes, sign)
|
||||
elseif keywords.dir[str] then
|
||||
return key_instance.new("dir", keywords.dir[str], sign)
|
||||
elseif keywords.mirroring[str] then
|
||||
return key_instance.new("rev", "mirroring")
|
||||
else return key_instance.new("err", "Error: \""..str.."\" is not a valid axis, direction or keyword.", 422)
|
||||
end
|
||||
end
|
||||
|
||||
--- Creates a vector with a length of (@param: value * @param: sign)
|
||||
-- on each axis in @param: axes.
|
||||
-- @param: axes: Table: List of axes to set
|
||||
-- @param: value: Number: length of to set axes
|
||||
-- @param: sign: Number: sign multiplier for axes
|
||||
-- @returns: Vector3: processed vector
|
||||
function parse.vectorize(axes,value,sign)
|
||||
local ret = Vector3.new()
|
||||
for i,v in ipairs(axes) do
|
||||
ret[v] = value * sign
|
||||
end
|
||||
return ret
|
||||
end
|
||||
|
||||
--- Converts Unified Axis Keyword table into Vector3 instances.
|
||||
-- @param: tbl: Table: Keyword table to parse
|
||||
-- @param: facing: Table: Output from worldeditadditions.player_dir(name)
|
||||
-- @param: sum: Bool: Return a single vector by summing the 2 output vectors together
|
||||
-- @returns: Vector3, [Vector3]: returns min, max Vector3s or sum Vector3 (if @param: sum ~= nil)
|
||||
-- if error: @returns: false, String: error message
|
||||
function parse.keytable(tbl, facing, sum)
|
||||
local min, max = Vector3.new(), Vector3.new()
|
||||
local expected, tmp = 1, {axes = {}, num = 0, sign = 1, mirror = false}
|
||||
function tmp:reset() self.axis, self.sign = "", 1 end
|
||||
|
||||
for i,v in ipairs(tbl) do
|
||||
if v:sub(1,1) == "+" then v = v:sub(2) end
|
||||
tmp.num = parse.num(v)
|
||||
if expected == 1 then -- Mode 1 of state machine
|
||||
-- State machine expects string
|
||||
if tmp.num then
|
||||
-- If this is a number treat as all axes and add to appropriate vector
|
||||
if tmp.num * tmp.sign >= 0 then
|
||||
max = max:add(parse.vectorize({"x","y","z"}, tmp.num, tmp.sign))
|
||||
else
|
||||
min = min:add(parse.vectorize({"x","y","z"}, tmp.num, tmp.sign))
|
||||
end
|
||||
-- We are still looking for axes so the state machine should remain
|
||||
-- in Mode 1 for the next iteration
|
||||
else
|
||||
-- Else parse.keyword
|
||||
local key_inst = parse.keyword(v)
|
||||
-- Stop if error and return message
|
||||
if key_inst:is_error() then return false, key_inst.entry end
|
||||
-- Check key type and process further
|
||||
if key_inst.type == "axis" then
|
||||
tmp.axes = key_inst.entry
|
||||
tmp.sign = key_inst.sign
|
||||
elseif key_inst.type == "dir" then
|
||||
tmp.axes = {facing[key_inst.entry].axis}
|
||||
tmp.sign = facing[key_inst.entry].sign * key_inst.sign
|
||||
elseif key_inst.type == "rev" then
|
||||
tmp.mirror = true
|
||||
else
|
||||
-- If key type is error or unknown throw error and stop
|
||||
if key_inst.type == "err" then
|
||||
return false, key_inst.entry
|
||||
else
|
||||
return false, "Error: Unknown Key Instance type \""..
|
||||
tostring(key_inst.type).."\". Contact the devs!"
|
||||
end
|
||||
end
|
||||
expected = 2 -- Toggle state machine to expect number (Mode 2)
|
||||
end
|
||||
|
||||
else -- Mode 2 of state machine
|
||||
-- State machine expects number
|
||||
if tmp.num then
|
||||
-- If this is a number process num and add to appropriate vector
|
||||
if tmp.num * tmp.sign >= 0 then
|
||||
max = max:add(parse.vectorize(tmp.axes, tmp.num, tmp.sign))
|
||||
else
|
||||
min = min:add(parse.vectorize(tmp.axes, tmp.num, tmp.sign))
|
||||
end
|
||||
expected = 1 -- Toggle state machine to expect string (Mode 1)
|
||||
else
|
||||
-- Else throw an error and stop everything
|
||||
return false, "Error: Expected number after \""..tostring(tbl[i-1])..
|
||||
"\", got \""..tostring(v).."\"."
|
||||
end
|
||||
end -- End of state machine
|
||||
|
||||
end -- End of main for loop
|
||||
|
||||
-- Handle Mirroring
|
||||
if tmp.mirror and not sum then
|
||||
max = max:max(min:abs())
|
||||
min = max:multiply(-1)
|
||||
end
|
||||
|
||||
if sum then return min:add(max)
|
||||
else return min, max end
|
||||
|
||||
end
|
||||
|
||||
return {
|
||||
keyword = parse.keyword,
|
||||
keytable = parse.keytable,
|
||||
}
|
|
@ -4,11 +4,18 @@
|
|||
-- ██ ██ ██ ██ ██ ██ ██
|
||||
-- ██ ██ ██ ██ ██ ███████ ███████
|
||||
|
||||
-- Unified Axes Keyword Parser
|
||||
local uak_parse = dofile(worldeditadditions.modpath.."/utils/parse/axes_parser.lua")
|
||||
-- Old axis parsing functions
|
||||
local axes = dofile(worldeditadditions.modpath.."/utils/parse/axes.lua")
|
||||
|
||||
worldeditadditions.parse = {
|
||||
direction_keyword = uak_parse.keyword,
|
||||
directions = uak_parse.keytable,
|
||||
-- Old parse functions (marked for deprecation).
|
||||
-- Use parse.keytable or parse.keyword instead
|
||||
axes = axes.parse_axes,
|
||||
axis_name = axes.parse_axis_name
|
||||
axis_name = axes.parse_axis_name,
|
||||
}
|
||||
|
||||
dofile(worldeditadditions.modpath.."/utils/parse/chance.lua")
|
||||
|
|
90
worldeditadditions/utils/parse/key_instance.lua
Normal file
90
worldeditadditions/utils/parse/key_instance.lua
Normal file
|
@ -0,0 +1,90 @@
|
|||
--- A container for transmitting (axis table, sign) or (dir, sign) pairs
|
||||
-- and other data within parsing functions.
|
||||
-- @class
|
||||
local key_instance = {}
|
||||
key_instance.__index = key_instance
|
||||
key_instance.__name = "Key Instance"
|
||||
|
||||
-- Allowed values for "type" field
|
||||
local types = {
|
||||
err = true, rev = true,
|
||||
axis = true, dir = true,
|
||||
replace = {
|
||||
error = "err",
|
||||
axes = "axis",
|
||||
},
|
||||
}
|
||||
|
||||
-- Simple function for putting stuff in quotes
|
||||
local function enquote(take)
|
||||
if type(take) == "string" then
|
||||
return '"'..take..'"'
|
||||
else return tostring(take) end
|
||||
end
|
||||
|
||||
--- Creates a new Key Instance.
|
||||
-- This is a table with a "type" string, an entry string or table
|
||||
-- and an optional signed integer (or code number in the case of errors)
|
||||
-- @param: type: String: Key type (axis, dir(ection), rev (mirroring) or error).
|
||||
-- @param: entry: String: The main content of the key.
|
||||
-- @param: sign: Int: The signed multiplier of the key (if any).
|
||||
-- @return: Key Instance: The new Key Instance.
|
||||
function key_instance.new(type,entry,sign)
|
||||
if types.replace[type] then
|
||||
type = types.replace[type]
|
||||
elseif not types[type] then
|
||||
return key_instance.new("err",
|
||||
"Key Instance internal error: Invalid type "..enquote(type)..".",
|
||||
500)
|
||||
end
|
||||
local tbl = {type = type, entry = entry}
|
||||
if sign and sign ~= 0 then
|
||||
if type == "err" then tbl.code = sign
|
||||
else tbl.sign = sign end
|
||||
end
|
||||
return setmetatable(tbl, key_instance)
|
||||
end
|
||||
|
||||
--- Checks if Key Instance "entry" field is a table.
|
||||
-- @param: tbl: Key Instance: The Key Instance to check.
|
||||
-- @return: Bool: Returns true if Key Instance has a non 0 sign value.
|
||||
function key_instance.entry_table(a)
|
||||
if type(a.entry) == "table" then
|
||||
return true
|
||||
else return false end
|
||||
end
|
||||
|
||||
--- Checks if Key Instance has a signed multiplier.
|
||||
-- @param: tbl: Key Instance: The Key Instance to check.
|
||||
-- @return: Bool: Returns true if Key Instance has a non 0 sign value.
|
||||
function key_instance.has_sign(a)
|
||||
if not a.sign or a.sign == 0 then
|
||||
return false
|
||||
else return true end
|
||||
end
|
||||
|
||||
--- Checks if Key Instance is an error.
|
||||
-- @param: tbl: Key Instance: The Key Instance to check.
|
||||
-- @return: Bool: Returns true if Key Instance is an error.
|
||||
function key_instance.is_error(a)
|
||||
if a.type == "err" then return true
|
||||
else return false end
|
||||
end
|
||||
|
||||
function key_instance.__tostring(a)
|
||||
local ret = "{type = "..enquote(a.type)..", entry = "
|
||||
if type(a.entry) == "table" and #a.entry <= 3 then
|
||||
ret = ret.."{"
|
||||
for _i,v in ipairs(a.entry) do
|
||||
ret = ret..enquote(v)..", "
|
||||
end
|
||||
ret = ret:sub(1,-3).."}"
|
||||
else ret = ret..enquote(a.entry) end
|
||||
|
||||
if a:is_error() and a.code then ret = ret..", code = "..a.code.."}"
|
||||
elseif a:has_sign() then ret = ret..", sign = "..a.sign.."}"
|
||||
else ret = ret.."}" end
|
||||
return ret
|
||||
end
|
||||
|
||||
return key_instance
|
Loading…
Reference in a new issue