diff --git a/worldeditadditions/utils/parse/axes_parser.lua b/worldeditadditions/utils/parse/axes_parser.lua new file mode 100644 index 0000000..409e2cd --- /dev/null +++ b/worldeditadditions/utils/parse/axes_parser.lua @@ -0,0 +1,204 @@ +-- ██████ █████ ██████ ███████ ███████ █████ ██ ██ ███████ ███████ +-- ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ +-- ██████ ███████ ██████ ███████ █████ ███████ ███ █████ ███████ +-- ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ +-- ██ ██ ██ ██ ██ ███████ ███████ ██ ██ ██ ██ ███████ ███████ + +-- 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.key_instance") +end + +--- Unified Axis Keywords banks +local keywords = {} +-- Direction keywords +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 +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.axes(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) + end + local sign = 1 + if str:sub(1,1) == "-" then + sign = -1 + str = str:sub(2) + end + + local axes = parse.axes(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 + elseif key_inst.type == "dir" then + tmp.axes = {facing[key_inst.entry].axis} + tmp.sign = facing[key_inst.entry].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 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 parse diff --git a/worldeditadditions/utils/parse/key_instance.lua b/worldeditadditions/utils/parse/key_instance.lua new file mode 100644 index 0000000..fc78a86 --- /dev/null +++ b/worldeditadditions/utils/parse/key_instance.lua @@ -0,0 +1,69 @@ +--- A container for transmitting axis, sign pairs within parsing functions. +-- @class +local key_instance = {} +key_instance.__index = key_instance +key_instance.__name = "Key Instance" + +--- Creates a new Key Instance. +-- @param: type: String: Key type (axis, direction, 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 type == "error" then type = "err" 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 function enquote(take) + if type(take) == "string" then + return '"'..take..'"' + else return tostring(take) end + end + 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