690 lines
26 KiB
Lua
690 lines
26 KiB
Lua
--local bc = better_commands
|
|
local S = minetest.get_translator(minetest.get_current_modname())
|
|
|
|
---Parses parameters out of a string
|
|
---@param str string
|
|
---@return splitParam[] split_param
|
|
function better_commands.parse_params(str)
|
|
local i = 1
|
|
local tmp
|
|
local found = {}
|
|
-- selectors, @?[data]
|
|
repeat
|
|
tmp = {str:find("(@[parsen])%s*(%[.-%])", i)}
|
|
if tmp[1] then
|
|
i = tmp[2] + 1
|
|
tmp.type = "selector"
|
|
tmp.extra_data = true
|
|
table.insert(found, table.copy(tmp))
|
|
end
|
|
until not tmp[1]
|
|
|
|
-- items/entities with extra data
|
|
i = 1
|
|
repeat
|
|
-- modname:id[data] count wear (everything but id and data optional)
|
|
tmp = {str:find("%s([_%w]*:?[_%w]+)%s*(%[.-%])%s*(%d*)%s*(%d*)", i)}
|
|
if tmp[1] then
|
|
tmp[1] = tmp[1] + 1 -- ignore the space
|
|
local overlap
|
|
for _, thing in pairs(found) do
|
|
if tmp[1] >= thing[1] and tmp[1] <= thing[2]
|
|
or tmp[2] >= thing[1] and tmp[2] <= thing[2]
|
|
or tmp[1] <= thing[1] and tmp[2] >= thing[2] then
|
|
overlap = true
|
|
break
|
|
end
|
|
end
|
|
i = tmp[2] + 1
|
|
if not overlap then
|
|
if better_commands.handle_alias(tmp[3]) then
|
|
tmp.type = "item"
|
|
tmp.extra_data = true
|
|
table.insert(found, table.copy(tmp))
|
|
elseif better_commands.entity_from_alias(tmp[3]) then
|
|
tmp.type = "entity"
|
|
tmp.extra_data = true
|
|
table.insert(found, table.copy(tmp))
|
|
end
|
|
end
|
|
end
|
|
until not tmp[1]
|
|
|
|
-- items/entities without extra data
|
|
i = 1
|
|
repeat
|
|
-- modname:id count wear (everything but id optional)
|
|
tmp = {str:find("(%s[_%w]*:?[_%w]+)%s*(%d*)%s*(%d*)", i)}
|
|
if tmp[1] then
|
|
tmp[1] = tmp[1] + 1 -- ignore the space
|
|
local overlap
|
|
for _, thing in pairs(found) do
|
|
if tmp[1] >= thing[1] and tmp[1] <= thing[2]
|
|
or tmp[2] >= thing[1] and tmp[2] <= thing[2]
|
|
or tmp[1] <= thing[1] and tmp[2] >= thing[2] then
|
|
overlap = true
|
|
break
|
|
end
|
|
end
|
|
i = tmp[2] + 1
|
|
if not overlap then
|
|
if better_commands.handle_alias(tmp[3]) then
|
|
tmp.type = "item"
|
|
table.insert(found, table.copy(tmp))
|
|
elseif better_commands.entity_from_alias(tmp[3]) then
|
|
tmp.type = "entity"
|
|
table.insert(found, table.copy(tmp))
|
|
end
|
|
end
|
|
end
|
|
until not tmp[1]
|
|
|
|
-- everything else
|
|
i = 1
|
|
repeat
|
|
tmp = {str:find("(%S+)", i)}
|
|
if tmp[1] then
|
|
i = tmp[2] + 1
|
|
local overlap
|
|
for _, thing in pairs(found) do
|
|
if tmp[1] >= thing[1] and tmp[1] <= thing[2]
|
|
or tmp[2] >= thing[1] and tmp[2] <= thing[2]
|
|
or tmp[1] <= thing[1] and tmp[2] >= thing[2] then
|
|
overlap = true
|
|
break
|
|
end
|
|
end
|
|
if not overlap then
|
|
if tmp[3]:find("^@[parsen]$") then
|
|
tmp.type = "selector"
|
|
elseif better_commands.players[tmp[3]] then
|
|
tmp.type = "selector"
|
|
elseif better_commands.handle_alias(tmp[3]) then
|
|
tmp.type = "item"
|
|
elseif tonumber(tmp[3]) then
|
|
tmp.type = "number"
|
|
elseif tmp[3]:lower() == "true" or tmp[3]:lower() == "false" then
|
|
tmp.type = "boolean"
|
|
elseif tmp[3]:find("^~%-?%d*%.?%d*$") then
|
|
tmp.type = "relative"
|
|
elseif tmp[3]:find("^%^%-?%d*%.?%d*$") then
|
|
tmp.type = "look_relative"
|
|
else
|
|
tmp.type = "string"
|
|
end
|
|
table.insert(found, table.copy(tmp))
|
|
end
|
|
end
|
|
until not tmp[1]
|
|
|
|
-- sort
|
|
table.sort(found, function(a,b)
|
|
return a[1] < b[1]
|
|
end)
|
|
return found
|
|
end
|
|
---Returns true if num is in the range string, false if not, nil on failure
|
|
---@param num number
|
|
---@param range string
|
|
---@return boolean?
|
|
function better_commands.parse_range(num, range)
|
|
if not (num and range) then return end
|
|
if tonumber(range) then return num == range end
|
|
-- "min..max" where both numbers are optional
|
|
local _, _, min, max = range:find("(%d*%.?%d*)%s*%.%.%s*(%d*%.?%d*)")
|
|
if not min then return end
|
|
min = tonumber(min)
|
|
max = tonumber(max)
|
|
if min and num < min then return false end
|
|
if max and num > max then return false end
|
|
return true
|
|
end
|
|
|
|
-- key = handle duplicates automatically?
|
|
better_commands.supported_keys = {
|
|
distance = true,
|
|
name = false,
|
|
type = false,
|
|
r = true,
|
|
rm = true,
|
|
sort = true,
|
|
limit = false,
|
|
c = false,
|
|
x = true,
|
|
y = true,
|
|
z = true,
|
|
gamemode = false,
|
|
m = false,
|
|
}
|
|
|
|
if better_commands.mcl then
|
|
local mcl_only = {
|
|
level = true,
|
|
l = true,
|
|
lm = true,
|
|
}
|
|
for key, value in pairs(mcl_only) do
|
|
better_commands.supported_keys[key] = value
|
|
end
|
|
end
|
|
|
|
---Parses a selector and returns a list of entities
|
|
---@param selector_data splitParam
|
|
---@param context contextTable
|
|
---@param require_one? boolean
|
|
---@return (minetest.ObjectRef|vector.Vector)[]? results
|
|
---@return string? err
|
|
---@nodiscard
|
|
function better_commands.parse_selector(selector_data, context, require_one)
|
|
local caller = context.executor
|
|
local pos = table.copy(context.pos)
|
|
local result = {}
|
|
if selector_data[3]:sub(1,1) ~= "@" then
|
|
local player = minetest.get_player_by_name(selector_data[3])
|
|
if not player then
|
|
return nil, S("No player was found")
|
|
else
|
|
return {player}
|
|
end
|
|
end
|
|
|
|
---@type table<integer, {any: string}>
|
|
local arg_table = {}
|
|
|
|
if selector_data[4] then
|
|
-- basically matching "(thing)=(thing)[,%]]"
|
|
for key, value in selector_data[4]:gmatch("([%w_]+)%s*=%s*([^,%]]+)%s*[,%]]") do
|
|
table.insert(arg_table, {key:trim(), value:trim()})
|
|
end
|
|
end
|
|
|
|
local objects = {}
|
|
local selector = selector_data[3]
|
|
if selector == "@s" then
|
|
return {caller}
|
|
end
|
|
-- Always include players
|
|
for _, player in pairs(minetest.get_connected_players()) do
|
|
if player:get_pos() then
|
|
table.insert(objects, player)
|
|
end
|
|
end
|
|
if selector == "@e" or selector == "@n" then
|
|
for _, luaentity in pairs(minetest.luaentities) do
|
|
if luaentity.object:get_pos() then
|
|
table.insert(objects, luaentity.object)
|
|
end
|
|
end
|
|
end
|
|
-- Make type selector work for @r
|
|
if selector == "@r" or selector == "@p" then
|
|
for _, arg in ipairs(arg_table) do
|
|
if arg[1] == "type" and arg[2]:lower() ~= "player" then
|
|
for _, luaentity in pairs(minetest.luaentities) do
|
|
if luaentity.object:get_pos() then
|
|
table.insert(objects, luaentity.object)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
local sort
|
|
if selector == "@p" or selector == "@n" then
|
|
sort = "nearest"
|
|
elseif selector == "@r" then
|
|
sort = "random"
|
|
else
|
|
sort = "arbitrary"
|
|
end
|
|
local limit
|
|
if selector == "@p" or selector == "@n" or selector == "@r" then limit = 1 end
|
|
|
|
if arg_table then
|
|
-- Look for pos first
|
|
local checked = {}
|
|
for _, arg in ipairs(arg_table) do
|
|
local key, value = unpack(arg)
|
|
if better_commands.supported_keys[key] == nil then
|
|
return nil, S("Unknown option '@1'", key)
|
|
elseif key == "x" or key == "y" or key == "z" then
|
|
if checked[key] then
|
|
return nil, S("Duplicate option '@1'", key)
|
|
end
|
|
if value:sub(1,1) == "!" then
|
|
value = value:sub(2,-1)
|
|
if value == "" then value = 0 end
|
|
end
|
|
checked[key] = true
|
|
pos[key] = tonumber(value)
|
|
if not pos[key] then
|
|
return nil, S("Expected number for option '@1'", key)
|
|
end
|
|
checked[key] = true
|
|
elseif key == "sort" then
|
|
sort = value
|
|
elseif key == "limit" or key == "c" then
|
|
if checked.limit then
|
|
return nil, S("Only 1 of keys c and limit can exist")
|
|
end
|
|
checked.limit = true
|
|
value = tonumber(value)
|
|
if not value then
|
|
return nil, S("@1 must be a non-zero integer", key)
|
|
end
|
|
limit = math.floor(value)
|
|
if limit == 0 then
|
|
return nil, S("@1 must be a non-zero integer", key)
|
|
end
|
|
end
|
|
end
|
|
|
|
for _, obj in pairs(objects) do
|
|
checked = {}
|
|
if obj.is_player then -- checks if it is a valid entity
|
|
local matches = true
|
|
for _, arg in pairs(arg_table) do
|
|
local key, value = unpack(arg)
|
|
if better_commands.supported_keys[key] == true then
|
|
if checked[key] then
|
|
return nil, S("Duplicate option '@1'", key)
|
|
end
|
|
checked[key] = true
|
|
end
|
|
if key == "distance" then
|
|
local distance = vector.distance(obj:get_pos(), pos)
|
|
if not better_commands.parse_range(distance, value) then
|
|
matches = false
|
|
end
|
|
elseif key == "type" then
|
|
value = value:lower()
|
|
local type_table = {}
|
|
if obj:is_player() then
|
|
type_table.player = true
|
|
else
|
|
local obj_type = obj:get_luaentity().name
|
|
local aliases = better_commands.entity_aliases[obj_type]
|
|
type_table = aliases and table.copy(aliases) or {}
|
|
type_table[obj_type] = true
|
|
end
|
|
|
|
if value:sub(1,1) == "!" then
|
|
if type_table[value:sub(2, -1)] then
|
|
matches = false
|
|
end
|
|
else
|
|
if checked.type then
|
|
return nil, S("Duplicate option '@1'", key)
|
|
end
|
|
checked.type = true
|
|
if not type_table[value] then
|
|
matches = false
|
|
end
|
|
end
|
|
elseif key == "name" then
|
|
local obj_name = better_commands.get_entity_name(obj, true, true)
|
|
if value:sub(1,1) == "!" then
|
|
if obj_name == value:sub(2, -1) then
|
|
matches = false
|
|
end
|
|
else
|
|
if checked.name then
|
|
return nil, S("Duplicate option '@1'", key)
|
|
end
|
|
checked.name = true
|
|
if obj_name ~= value then
|
|
matches = false
|
|
end
|
|
end
|
|
elseif key == "r" then
|
|
value = tonumber(value)
|
|
if not value then return nil, S("Expected number for option '@1'", key) end
|
|
matches = vector.distance(obj:get_pos(), pos) <= value
|
|
elseif key == "rm" then
|
|
value = tonumber(value)
|
|
if not value then return nil, S("Expected number for option '@1'", key) end
|
|
matches = vector.distance(obj:get_pos(), pos) >= value
|
|
elseif key == "level" then
|
|
if not (obj.is_player and obj:is_player()) then
|
|
matches = false
|
|
else
|
|
mcl_experience.update(obj)
|
|
local level = mcl_experience.get_level(obj)
|
|
if not better_commands.parse_range(level, value) then
|
|
matches = false
|
|
end
|
|
end
|
|
elseif key == "l" then
|
|
value = tonumber(value)
|
|
if not value then return nil, S("Expected number for option '@1'", key) end
|
|
if not (obj.is_player and obj:is_player()) then
|
|
matches = false
|
|
else
|
|
mcl_experience.update(obj)
|
|
local level = mcl_experience.get_level(obj)
|
|
matches = level <= value
|
|
end
|
|
elseif key == "lm" then
|
|
value = tonumber(value)
|
|
if not value then return nil, S("Expected number for option '@1'", key) end
|
|
if not (obj.is_player and obj:is_player()) then
|
|
matches = false
|
|
else
|
|
mcl_experience.update(obj)
|
|
local level = mcl_experience.get_level(obj)
|
|
matches = level >= value
|
|
end
|
|
elseif key == "gamemode" or key == "m" then
|
|
if checked.gamemode then
|
|
return nil, S("Only 1 of keys m and gamemode can exist")
|
|
end
|
|
checked.gamemode = true
|
|
if not (obj.is_player and obj:is_player()) then
|
|
matches = false
|
|
else
|
|
local gamemode = better_commands.gamemode_aliases[value] or value
|
|
if better_commands.mcl then
|
|
if table.indexof(mcl_gamemode.gamemodes, gamemode) == -1 then
|
|
return nil, S("Unknown game mode: @1", gamemode)
|
|
end
|
|
elseif gamemode ~= "creative" and gamemode ~= "survival" then
|
|
return nil, S("Unknown game mode: @1", gamemode)
|
|
end
|
|
matches = better_commands.get_gamemode(obj) == gamemode
|
|
end
|
|
end
|
|
if not matches then
|
|
break
|
|
end
|
|
end
|
|
if matches then
|
|
table.insert(result, obj)
|
|
end
|
|
end
|
|
end
|
|
else
|
|
result = objects
|
|
end
|
|
-- Sort
|
|
if sort == "random" then
|
|
table.shuffle(result)
|
|
elseif sort == "nearest" or (sort == "furthest" and limit < 0) then
|
|
table.sort(result, function(a,b) return vector.distance(a:get_pos(), pos) < vector.distance(b:get_pos(), pos) end)
|
|
elseif sort == "furthest" or (sort == "nearest" and limit < 0) then
|
|
table.sort(result, function(a,b) return vector.distance(a:get_pos(), pos) > vector.distance(b:get_pos(), pos) end)
|
|
end
|
|
-- Limit
|
|
if limit then
|
|
local new_result = {}
|
|
local i = 1
|
|
while i <= limit do
|
|
if not result[i] then break end
|
|
table.insert(new_result, result[i])
|
|
i = i + 1
|
|
end
|
|
result = new_result
|
|
end
|
|
if require_one then
|
|
if #result == 0 then
|
|
return nil, S("No entity was found")
|
|
elseif #result > 1 then
|
|
return nil, S("Multiple matching entities found")
|
|
end
|
|
end
|
|
|
|
return result
|
|
end
|
|
|
|
---Parses a position
|
|
---@param data splitParam[]
|
|
---@param start integer
|
|
---@param context contextTable
|
|
---@return vector.Vector? result
|
|
---@return string? err
|
|
---@nodiscard
|
|
function better_commands.parse_pos(data, start, context)
|
|
local axes = {"x","y","z"}
|
|
local result = table.copy(context.pos)
|
|
local look
|
|
for i = 0, 2 do
|
|
if not data[start + i] then
|
|
return nil, S("Missing coordinate")
|
|
end
|
|
local coordinate, _type = data[start + i][3], data[start + i].type
|
|
if _type == "number" or tonumber(coordinate) then
|
|
if look then
|
|
return nil, S("Cannot mix local and global coordinates")
|
|
end
|
|
result[axes[i+1]] = tonumber(coordinate)
|
|
look = false
|
|
elseif _type == "relative" then
|
|
if look then
|
|
return nil, S("Cannot mix local and global coordinates")
|
|
end
|
|
result[axes[i+1]] = result[axes[i+1]] + (tonumber(coordinate:sub(2,-1)) or 0)
|
|
look = false
|
|
elseif _type == "look_relative" then
|
|
if look == false then
|
|
return nil, S("Cannot mix local and global coordinates")
|
|
end
|
|
result[axes[i+1]] = tonumber(coordinate:sub(2,-1)) or 0
|
|
look = true
|
|
else
|
|
return nil, S("Invalid coordinate '@1'", coordinate)
|
|
end
|
|
end
|
|
if look then
|
|
-- There's almost definitely a better way to do this...
|
|
-- All I know is when moving in the Y direction,
|
|
-- X/Z are backwards, and when moving in the Z direction,
|
|
-- Y is backwards... so I fixed it (probably badly)
|
|
local result_x = vector.rotate(vector.new(result.x,0,0), context.rot)
|
|
local result_y = vector.rotate(vector.new(0,result.y,0), context.rot)
|
|
result_y.z = -result_y.z
|
|
result_y.x = -result_y.x
|
|
local result_z = vector.rotate(vector.new(0,0,result.z), context.rot)
|
|
result_z.y = -result_z.y
|
|
result = vector.add(vector.add(vector.add(context.pos, result_x), result_y), result_z)
|
|
end
|
|
return result
|
|
end
|
|
|
|
---Parses item data, returning an itemstack or err message
|
|
---@param item_data splitParam
|
|
---@return minetest.ItemStack? result
|
|
---@return string? err
|
|
---@nodiscard
|
|
function better_commands.parse_item(item_data, ignore_count)
|
|
if not better_commands.handle_alias(item_data[3]) then
|
|
return nil, S("Invalid item '@1'", item_data[3])
|
|
end
|
|
if item_data.type == "item" and not item_data.extra_data then
|
|
local stack = ItemStack(item_data[3])
|
|
if not ignore_count then
|
|
stack:set_count(tonumber(item_data[4]) or 1)
|
|
end
|
|
stack:set_wear(tonumber(item_data[5]) or 0)
|
|
return stack
|
|
elseif item_data.type == "item" then
|
|
local arg_table = {}
|
|
if item_data[4] then
|
|
-- basically matching (thing)=(thing) followed by , or ]
|
|
for key, value in item_data[4]:gmatch("([%w_]+)%s*=%s*([^,%]]+)%s*[,%]]") do
|
|
arg_table[key:trim()] = value:trim()
|
|
end
|
|
end
|
|
local stack = ItemStack(item_data[3])
|
|
if arg_table then
|
|
local meta = stack:get_meta()
|
|
for key, value in pairs(arg_table) do
|
|
if key == "wear" then
|
|
stack:set_wear(tonumber(value) or 0)
|
|
else
|
|
meta:set_string(key, value)
|
|
end
|
|
end
|
|
end
|
|
if not ignore_count then
|
|
stack:set_count(tonumber(item_data[5]) or 1)
|
|
end
|
|
stack:set_wear(tonumber(item_data[6]) or stack:get_wear())
|
|
return stack
|
|
end
|
|
return nil, S("Invalid item: '@1'", item_data[3])
|
|
end
|
|
|
|
---Parses node data, returns node and metadata table
|
|
---@param item_data splitParam
|
|
---@return minetest.Node? node
|
|
---@return table? metadata
|
|
---@return string? err
|
|
---@nodiscard
|
|
function better_commands.parse_node(item_data)
|
|
if not item_data or item_data.type ~= "item" then
|
|
return nil, nil, S("Invalid item")
|
|
end
|
|
local itemstring = better_commands.handle_alias(item_data[3])
|
|
if not itemstring then
|
|
return nil, nil, S("Unknown node '@1'", item_data[3])
|
|
end
|
|
if not minetest.registered_nodes[itemstring] then
|
|
return nil, nil, S("'@1' is not a node", itemstring)
|
|
end
|
|
if item_data.type == "item" and not item_data.extra_data then
|
|
return {name = itemstring}
|
|
elseif item_data.type == "item" then
|
|
local meta_table = {}
|
|
local node_table = {name = itemstring}
|
|
if item_data[4] then
|
|
-- basically matching "(thing)=(thing)[,%]]"
|
|
for key, value in item_data[4]:gmatch("([%w_]+)%s*=%s*([^,%]]+)%s*[,%]]") do
|
|
local trimmed_key, trimmed_value = key:trim(), value:trim()
|
|
if trimmed_key == "param1" or trimmed_key == "param2" then
|
|
node_table[trimmed_key] = trimmed_value
|
|
else
|
|
meta_table[trimmed_key] = trimmed_value
|
|
end
|
|
end
|
|
end
|
|
return node_table, meta_table
|
|
end
|
|
return nil, nil, S("Invalid item '@1'", item_data[3])
|
|
end
|
|
|
|
---Parses a time string and returns a time (between 0 and 1)
|
|
---@param time string The time string to parse
|
|
---@param absolute? boolean Whether to add the result to the current time or not
|
|
---@return number? result
|
|
---@return string? err
|
|
---@nodiscard
|
|
function better_commands.parse_time_string(time, absolute)
|
|
local result
|
|
if better_commands.times[time] then return better_commands.times[time] end
|
|
local amount, unit = time:match("^(%d*%.?%d+)(.?)$")
|
|
if not amount or not tonumber(amount) then
|
|
local hours, minutes = time:match("^([0-2]?%d):([0-5]%d)$")
|
|
if not hours then
|
|
return nil, S("Invalid amount")
|
|
end
|
|
amount = (tonumber(hours) + tonumber(minutes)/60) * 1000
|
|
unit = "t"
|
|
else
|
|
if unit == "" then unit = "t" end
|
|
amount = tonumber(amount)
|
|
end
|
|
-- The pattern shouldn't let through any negative numbers... but just in case
|
|
if amount < 0 then return nil, S("Amount must not be negative") end
|
|
if unit == "s" then
|
|
local tps = tonumber(minetest.settings:get("time_speed")) or 72
|
|
amount = amount * tps / 3.6 -- (3.6s = 1 millihour)
|
|
elseif unit == "d" then
|
|
amount = amount * 24000
|
|
elseif unit ~= "t" then
|
|
return nil, S("Invalid unit '@1'", unit)
|
|
end
|
|
|
|
if not absolute then
|
|
result = (minetest.get_timeofday() + (amount/24000)) % 1
|
|
elseif better_commands.settings.acovg_time then
|
|
result = ((amount + 6000)/24000) % 1
|
|
else
|
|
result = (amount/24000) % 1
|
|
end
|
|
|
|
return result
|
|
end
|
|
|
|
---Takes command parameters (with the split version) and expands all selectors
|
|
---@param str string
|
|
---@param split_param splitParam[]
|
|
---@param index integer
|
|
---@param context contextTable
|
|
---@return string? result
|
|
---@return string? err
|
|
---@nodiscard
|
|
function better_commands.expand_selectors(str, split_param, index, context)
|
|
local message = ""
|
|
for i=index,#split_param do
|
|
local data = split_param[i]
|
|
local next_part = ""
|
|
if data.type ~= "selector" then
|
|
if split_param[i+1] then
|
|
---@diagnostic disable-next-line: param-type-mismatch
|
|
next_part = str:sub(split_param[i][1], split_param[i+1][1]-1)
|
|
else
|
|
---@diagnostic disable-next-line: param-type-mismatch
|
|
next_part = str:sub(split_param[i][1], -1)
|
|
end
|
|
else
|
|
local targets, err = better_commands.parse_selector(data, context)
|
|
if err or not targets then
|
|
return nil, err
|
|
end
|
|
for j, obj in ipairs(targets) do
|
|
if j > 1 then next_part = next_part.." " end
|
|
if not obj.is_player then -- this should only happen with @s
|
|
next_part = next_part..S("Command Block")
|
|
break
|
|
end
|
|
next_part = next_part..better_commands.get_entity_name(obj)
|
|
if #targets == 1 then
|
|
break
|
|
elseif j < #targets then
|
|
next_part = next_part..","
|
|
end
|
|
end
|
|
if split_param[i+1] then
|
|
if split_param[i][2]+1 < split_param[i+1][1] then
|
|
next_part = next_part..str:sub(split_param[i][2]+1, split_param[i+1][1]-1)
|
|
end
|
|
elseif split_param[i][2] < #str then
|
|
next_part = next_part..str:sub(split_param[i][2]+1, -1)
|
|
end
|
|
end
|
|
message = message..next_part
|
|
end
|
|
return message
|
|
end
|
|
|
|
---Handles item aliases
|
|
---@param itemstring string
|
|
---@return string|false itemstring corrected itemstring if valid, otherwise false
|
|
function better_commands.handle_alias(itemstring)
|
|
local stack = ItemStack(itemstring)
|
|
if (stack:is_known() and stack:get_name() ~= "unknown" and stack:get_name() ~= "") then
|
|
return stack:get_name()
|
|
end
|
|
return false
|
|
end
|
|
|
|
---I wish #table would work for non-arrays...
|
|
---@param table table
|
|
---@return integer?
|
|
function better_commands.count_table(table)
|
|
if type(table) ~= "table" then return end
|
|
local count = 0
|
|
for _ in pairs(table) do
|
|
count = count + 1
|
|
end
|
|
return count
|
|
end |