Removed all references to ACOVG (recreated repo)

This commit is contained in:
ThePython 2024-05-05 14:46:18 -07:00
commit 24dee29e84
32 changed files with 4529 additions and 0 deletions

51
.luacheckrc Normal file
View File

@ -0,0 +1,51 @@
read_globals = {
"DIR_DELIM", "INIT",
"minetest", "core",
"dump", "dump2",
"Raycast",
"Settings",
"PseudoRandom",
"PerlinNoise",
"VoxelManip",
"SecureRandom",
"VoxelArea",
"PerlinNoiseMap",
"PcgRandom",
"ItemStack",
"AreaStore",
"vector",
"mcl_util",
table = {
fields = {
"copy",
"indexof",
"insert_all",
"key_value_swap",
"shuffle",
}
},
string = {
fields = {
"split",
"trim",
}
},
math = {
fields = {
"hypot",
"sign",
"factorial"
}
},
}
globals = {
"better_commands",
}

33
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,33 @@
{
"Lua.workspace.library": [
"C:\\portable\\apps\\minetest\\builtin",
"C:/Users/Nolan/AppData/Roaming/Code/User/globalStorage/sumneko.lua/addonManager/addons/minetest/module/library"
],
"Lua.diagnostics.disable": [
"undefined-field",
"cast-local-type"
],
"Lua.diagnostics.globals": [
"mcl_formspec",
"mcl_item_id",
"armor",
"bucket",
"mcl_buckets",
"mcl_armor",
"mcl_enchanting",
"default",
"mcl_sounds",
"mcl_util",
"mcl_potions",
"mcl_vars",
"playerphysics",
"mcl_playerinfo",
"mcl_bamboo",
"mcl_core",
"dump",
"better_commands",
"ItemStack",
"core",
"better_command_blocks"
],
}

61
API/API.lua Normal file
View File

@ -0,0 +1,61 @@
---@alias contextTable {executor: minetest.ObjectRef, pos: vector.Vector, rot: vector.Vector, anchor: string, origin: string, [any]: any}
---@alias splitParam {[1]: integer, [2]: integer, [3]: string, type: string, any: string}
---@alias betterCommandFunc fun(name: string, param: string, context: contextTable): success: boolean, message: string?, count: number
---@alias betterCommandDef {description: string, param?: string, privs: table<string, boolean>, func: betterCommandFunc}
--local bc = better_commands
local storage = minetest.get_mod_storage()
local modpath = minetest.get_modpath("better_commands")
function better_commands.run_file(file, subfolder)
dofile(string.format("%s%s%s.lua", modpath, subfolder and "/"..subfolder.."/" or "", file))
end
local api_files = {
"damage",
"entity",
"parsing",
"register",
"scoreboard",
"teams",
}
for _, file in ipairs(api_files) do
better_commands.run_file(file, "API")
end
local scoreboard_string = storage:get_string("scoreboard")
if scoreboard_string and scoreboard_string ~= "" then
better_commands.scoreboard = minetest.deserialize(scoreboard_string)
else
better_commands.scoreboard = {objectives = {}, displays = {colors = {}}}
end
local team_string = storage:get_string("teams")
if team_string and team_string ~= "" then
better_commands.teams = minetest.deserialize(team_string)
else
better_commands.teams = {teams = {}, players = {}}
end
local timer = 0
minetest.register_globalstep(function(dtime)
timer = timer + dtime
if timer > better_commands.save_interval then
timer = 0
storage:set_string("scoreboard", minetest.serialize(better_commands.scoreboard))
storage:set_string("teams", minetest.serialize(better_commands.teams))
better_commands.update_hud()
end
end)
minetest.register_on_shutdown(function()
storage:set_string("scoreboard", minetest.serialize(better_commands.scoreboard))
storage:set_string("teams", minetest.serialize(better_commands.teams))
storage:set_string("successful_shutdown", "true")
end)
minetest.register_on_joinplayer(function(player)
better_commands.sidebars[player:get_player_name()] = {}
end)

123
API/damage.lua Normal file
View File

@ -0,0 +1,123 @@
--local bc = better_commands
---Deals damage; copied from Mineclonia's mcl_util.deal_damage
---@param target minetest.ObjectRef
---@param damage integer
---@param reason table?
---@param damage_immortal? boolean
function better_commands.deal_damage(target, damage, reason, damage_immortal)
local luaentity = target:get_luaentity()
if luaentity then
if luaentity.deal_damage then -- Mobs Redo/Mobs MC
luaentity:deal_damage(damage, reason or {type = "generic"})
minetest.log("deal_damage")
return
elseif luaentity.hurt then -- Animalia
luaentity:hurt(damage)
minetest.log("hurt")
luaentity:indicate_damage()
return
elseif luaentity.health then -- Mobs Redo/Mobs MC/NSSM
-- local puncher = mcl_reason and mcl_reason.direct or target
-- target:punch(puncher, 1.0, {full_punch_interval = 1.0, damage_groups = {fleshy = damage}}, vector.direction(puncher:get_pos(), target:get_pos()), damage)
if luaentity.health > 0 then
minetest.log("luaentity.health")
luaentity.health = luaentity.health - damage
end
return
end
end
local hp = target:get_hp()
local armorgroups = target:get_armor_groups()
if hp > 0 and armorgroups and (damage_immortal or not armorgroups.immortal) then
minetest.log("set_hp")
target:set_hp(hp - damage, reason)
end
end
minetest.register_on_dieplayer(function(player, reason)
local player_name = player:get_player_name()
for _, def in pairs(better_commands.scoreboard.objectives) do
if def.criterion == "deathCount" then
if def.scores[player_name] then
def.scores[player_name].score = def.scores[player_name].score + 1
end
end
end
local killer
if reason._mcl_reason then
killer = reason._mcl_reason.source
else
killer = reason.object
end
if killer and killer:is_player() then
local player_name = player:get_player_name()
local killer_name = killer:get_player_name()
local player_team = better_commands.teams.players[player_name]
local killer_team = better_commands.teams.players[killer_name]
for _, def in pairs(better_commands.scoreboard.objectives) do
if def.criterion == "playerKillCount" or (player_team and def.criterion == "teamkill."..player_team) then
if def.scores[killer_name] then
def.scores[killer_name].score = def.scores[killer_name].score + 1
end
elseif killer_team and def.criterion == "killedByTeam."..killer_team then
if def.scores[player_name] then
def.scores[player_name].score = def.scores[player_name].score + 1
end
elseif def.criterion == "killed_by.player" then
if def.scores[player_name] then
def.scores[player_name].score = def.scores[player_name].score + 1
end
end
end
elseif killer then
local killer_type = killer:get_luaentity().name
for _, def in pairs(better_commands.scoreboard.objectives) do
local killed_by = def.criterion:match("^killed_by%.(.*)$")
if killed_by and (killer_type == killed_by or
(better_commands.entity_aliases[killer_type] and better_commands.entity_aliases[killer_type][killed_by])) then
if def.scores[player_name] then
def.scores[player_name].score = def.scores[player_name].score + 1
end
end
end
end
end)
-- Make sure players always die when /killed, also track hp
minetest.register_on_player_hpchange(function(player, hp_change, reason)
if reason.better_commands == "kill" then
return -player:get_properties().hp_max, true
end
local player_name = player:get_player_name()
for _, def in pairs(better_commands.scoreboard.objectives) do
if def.criterion == "health" then
if def.scores[player_name] then
minetest.after(0, function() def.scores[player_name].score = player:get_hp() end)
end
end
end
if hp_change < 0 then
local attacker
if reason._mcl_reason then
attacker = reason._mcl_reason.source
else
attacker = reason.object
end
if attacker and attacker:is_player() then
local player_name = player:get_player_name()
local attacker_name = attacker:get_player_name()
local player_team = better_commands.teams.players[player_name]
local attacker_team = better_commands.teams.players[attacker_name]
if player_team == attacker_team then
if better_commands.teams.teams[player_team].pvp == false then
return 0, true
end
end
end
end
return hp_change
end, true)

161
API/entity.lua Normal file
View File

@ -0,0 +1,161 @@
--local bc = better_commands
local S = minetest.get_translator(minetest.get_current_modname())
---Gets the name of an entity
---@param obj minetest.ObjectRef|vector.Vector
---@param no_id? boolean
---@return string
function better_commands.get_entity_name(obj, no_id, no_format)
if not obj.is_player then
return S("Command Block")
end
if obj:is_player() then
local player_name = obj:get_player_name()
if no_format then return player_name end
return better_commands.format_name(obj:get_player_name())
else
local luaentity = obj:get_luaentity()
if luaentity then
if no_id then
return luaentity._nametag or luaentity.nametag or ""
else
local name = luaentity._nametag or luaentity.nametag
if (not name) or name == "" then
name = luaentity.name
if name == "__builtin:item" then
local stack = ItemStack(luaentity.itemstring)
if not stack:is_known() then return S("Unknown Item") end
return stack:get_short_description()
elseif name == "__builtin:falling_node" then
local stack = ItemStack(luaentity.node.name)
if not stack:is_known() then return S("Unknown Falling Node") end
return S("Falling @1", stack:get_short_description())
end
return luaentity.description or better_commands.entity_names[name] or name
else
return name
end
end
else
return S("???")
end
end
end
---Gets an entity's current rotation
---@param obj minetest.ObjectRef|vector.Vector
---@return vector.Vector
function better_commands.get_entity_rotation(obj)
if obj.is_player and obj:is_player() then
return {x = obj:get_look_vertical(), y = obj:get_look_horizontal(), z = 0}
elseif obj.get_rotation then
return obj:get_rotation()
else
return vector.zero()
end
end
---Sets an entity's rotation
---@param obj minetest.ObjectRef|any
---@param rotation vector.Vector
function better_commands.set_entity_rotation(obj, rotation)
if not obj.is_player then return end
if obj:is_player() then
obj:set_look_vertical(rotation.x)
obj:set_look_horizontal(rotation.y)
elseif obj.set_rotation then
obj:set_rotation(rotation)
end
end
---Takes an object and a position, returns the rotation at which the object points at the position
---@param obj minetest.ObjectRef|vector.Vector
---@param pos vector.Vector
---@return vector.Vector
function better_commands.point_at_pos(obj, pos)
local obj_pos = obj.get_pos and obj:get_pos() or obj
if obj:is_player() then
obj_pos.y = obj_pos.y + obj:get_properties().eye_height
end
---@diagnostic disable-next-line: param-type-mismatch
local result = vector.dir_to_rotation(vector.direction(obj_pos, pos))
result.x = -result.x -- no clue why this is necessary
return result
end
---Completes a context table
---@param name string The name of the player to use as context.executor if not supplied
---@param context? table The context table to complete (optional)
---@return contextTable?
function better_commands.complete_context(name, context)
if not context then context = {} end
context.executor = context.executor or minetest.get_player_by_name(name)
if not context.executor then minetest.log("error", "Missing executor") return end
context.pos = context.pos or context.executor:get_pos()
context.rot = context.rot or better_commands.get_entity_rotation(context.executor)
context.anchor = context.anchor or "feet"
context.origin = context.origin or name
return context
end
function better_commands.entity_from_alias(alias, list)
if minetest.registered_entities[alias] then return alias end
local entities = better_commands.unique_entities[alias]
if not entities then return end
if list then return entities end
return entities[math.random(1, #entities)]
end
---Handles rotation when summoning/teleporting
---@param context contextTable
---@param victim minetest.ObjectRef|vector.Vector
---@param split_param splitParam[]
---@param i integer
---@return vector.Vector? result
---@return string? err
---@nodiscard
function better_commands.get_tp_rot(context, victim, split_param, i)
local victim_rot = table.copy(context.rot)
if split_param[i] then
local yaw_pitch
local facing
if split_param[i].type == "number" then
victim_rot.y = math.rad(tonumber(split_param[i][3]) or 0)
yaw_pitch = true
elseif split_param[i].type == "relative" then
victim_rot.y = victim_rot.y+math.rad(tonumber(split_param[i][3]:sub(2,-1)) or 0)
yaw_pitch = true
elseif split_param[i].type == "string" and split_param[i][3] == "facing" then
facing = true
end
if yaw_pitch and split_param[i+1] then
if split_param[i+1].type == "number" then
victim_rot.x = math.rad(tonumber(split_param[i+1][3]) or 0)
elseif split_param[i+1].type == "relative" then
victim_rot.x = victim_rot.x+math.rad(tonumber(split_param[i+1][3]:sub(2,-1)) or 0)
end
elseif facing and split_param[i+1] then
if split_param[i+1].type == "selector" then
local targets, err = better_commands.parse_selector(split_param[i+1], context, true)
if err or not targets then return nil, err end
local target_pos = targets[1].is_player and targets[1]:get_pos() or targets[1]
---@diagnostic disable-next-line: param-type-mismatch
victim_rot = better_commands.point_at_pos(victim, target_pos)
elseif split_param[i+1][3] == "entity" and split_param[i+2].type == "selector" then
local targets, err = better_commands.parse_selector(split_param[i+2], context, true)
if err or not targets then return nil, err end
local target_pos = targets[1].is_player and targets[1]:get_pos() or targets[1]
---@diagnostic disable-next-line: param-type-mismatch
victim_rot = better_commands.point_at_pos(victim, target_pos)
else
local target_pos, err = better_commands.parse_pos(split_param, i+1, context)
if err or not target_pos then return nil, err end
victim_rot = better_commands.point_at_pos(victim, target_pos)
end
end
if yaw_pitch or facing then
return victim_rot
end
end
return victim_rot
end

617
API/parsing.lua Normal file
View File

@ -0,0 +1,617 @@
--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("(@[psaer])%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] (modname 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
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("^@[psaer]$") 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,
}
---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("Player @1 not found", selector_data[3])
else
return {player}
end
end
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
if selector == "@e" or selector == "@a" or selector == "@p" or selector == "@r" then
for _, player in pairs(minetest.get_connected_players()) do
if player:get_pos() then
table.insert(objects, player)
end
end
end
if selector == "@e" 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" then
sort = "nearest"
elseif selector == "@r" then
sort = "random"
else
sort = "arbitrary"
end
local limit
if selector == "@p" 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 key == "x" or key == "y" or key == "z" then
if checked[key] then
return nil, S("Duplicate key: @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("Invalid value for @1", key)
end
checked[key] = true
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] == nil then
return nil, S("Invalid key: @1", key)
elseif better_commands.supported_keys[key] == true then
if checked[key] then
return nil, S("Duplicate key: @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 key: @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 key: @1", key)
end
checked.name = true
if obj_name ~= value then
matches = false
end
end
elseif key == "r" then
matches = vector.distance(obj:get_pos(), pos) < value
elseif key == "rm" then
matches = vector.distance(obj:get_pos(), pos) > value
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
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 matching entities 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)
if not context then
return nil, S("Missing context")
end
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)
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])
stack:set_count(tonumber(item_data[4]) or 1)
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)[,%]]"
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
meta:set_string(key, value)
end
end
stack:set_count(tonumber(item_data[5]) or 1)
stack:set_wear(tonumber(item_data[6]) or 1)
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("Invalid item: @1", item_data[3])
end
if not minetest.registered_nodes[itemstring] then
return nil, nil, S("Not a node: @1", 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
-- Don't think it's even possible to be negative but just in case
if amount < 0 then return nil, S("Amount must not be negative") end
if unit == "s" then
local second_multiplier = tonumber(minetest.settings:get("time_speed")) or 72
amount = amount * second_multiplier / 3.6 -- (3.6s = 1 millihour)
elseif unit == "d" then
amount = amount * 24000
elseif unit ~= "t" then
return nil, S("Unit must be either t (default), s, or d, not @1", unit)
end
minetest.log(amount)
if not absolute then
result = (minetest.get_timeofday() + (amount/24000)) % 1
elseif better_commands.mc_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
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? 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
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

36
API/register.lua Normal file
View File

@ -0,0 +1,36 @@
--local bc = better_commands
---Registers an ACOVG command
---@param name string The name of the command (/<name>)
---@param def betterCommandDef The command definition
function better_commands.register_command(name, def)
better_commands.commands[name] = def
end
---Registers an alias for an ACOVG command
---@param new string The name of the alias
---@param old string The original command
function better_commands.register_command_alias(new, old)
better_commands.register_command(new, better_commands.commands[old])
end
-- Register commands last (so overriding works properly)
minetest.register_on_mods_loaded(function()
for name, def in pairs(better_commands.commands) do
if minetest.registered_chatcommands[name] then
if better_commands.override then
minetest.log("action", "[Better Commands] Overriding "..name)
better_commands.old_commands[name] = minetest.registered_chatcommands[name]
minetest.unregister_chatcommand(name, def)
else
minetest.log("action", "[Better Commands] Not registering "..name.." as it already exists.")
return
end
end
minetest.register_chatcommand(name, def)
-- Since this is in an on_mods_loaded function, mod_origin is "??" by default
---@diagnostic disable-next-line: inject-field
minetest.registered_chatcommands[name].mod_origin = "better_commands"
end
end)

226
API/scoreboard.lua Normal file
View File

@ -0,0 +1,226 @@
--local bc = better_commands
local S = minetest.get_translator(minetest.get_current_modname())
better_commands.criteria_patterns = {
"^killed_by%..*$", -- killed_by.<entity name>
"^teamkill%..*$", -- teamkill.<team name>
"^killedByTeam%..*$", -- killedByTeam. <team name>
--"^distanceTo%.%-?%d*%.?%d+,%-?%d*%.?%d+,%-?%d*%.?%d+$" -- distanceTo.<x>,<y>,<z>
}
function better_commands.validate_criterion(criterion)
if better_commands.valid_criteria[criterion] then
return true
end
for _, pattern in ipairs(better_commands.criteria_patterns) do
if criterion:match(pattern) then
return true
end
end
end
better_commands.valid_criteria = {
dummy = true,
trigger = true,
deathCount = true,
playerKillCount = true,
health = true,
--xp = better_commands.mcl and true,
--level = better_commands.mcl and true,
--food = (better_commands.mcl or minetest.get_modpath("stamina") and true),
--air = true,
--armor = (better_commands.mcl or minetest.get_modpath("3d_armor") and true)
}
---Gets matching names in a scoreboard
---@param selector splitParam
---@param context contextTable
---@param objective string?
---@param require_one boolean?
---@return table<string, true>? result
---@return string? err
---@nodiscard
function better_commands.get_scoreboard_names(selector, context, objective, require_one)
local result = {}
local objectives = better_commands.scoreboard.objectives
if objective and not objectives[objective] then
return nil, S("Invalid objective: @1", objective)
end
if selector[3] == "*" then
if objective then
for name in pairs(objectives[objective].scores) do
result[name] = true
end
else
for _, data in pairs(objectives) do
for name in pairs(data.scores) do
result[name] = true
end
end
end
elseif selector.type == "selector" and selector[3]:sub(1,1) == "@" then
local targets, err = better_commands.parse_selector(selector, context)
if err or not targets then return nil, err end
for _, target in ipairs(targets) do
if target.is_player and target:is_player() then
result[target:get_player_name()] = true
end
end
else
result = {[selector[3]] = true}
end
local result_count = better_commands.count_table(result)
if result_count < 1 then
return nil, S("No targets found")
end
if require_one then
if result_count > 1 then
return nil, S("Multiple targets found")
else
result = {next(result)}
return {result[1]}
end
else
return result
end
end
local sidebar_template = {
bg = {
hud_elem_type = "image",
position = {x = 1, y = 0.5},
alignment = {x = 0, y = 1},
offset = {x = -70, y = 0},
text = "better_commands_scoreboard_bg.png",
scale = {x = 10, y = 10},
z_index = -1
},
title = {
hud_elem_type = "text",
text = "Title",
position = {x = 1, y = 0.5},
alignment = {x = 0, y = -1},
offset = {x = -70, y = 10},
number = 0xffffff,
},
names = {
hud_elem_type = "text",
position = {x = 1, y = 0.5},
alignment = {x = 1, y = 1},
offset = {x = -120, y = 0},
text = "Score\nScore2",
number = 0xffffff,
},
scores = {
hud_elem_type = "text",
position = {x = 1, y = 0.5},
alignment = {x = -1, y = 1},
offset = {x = -20, y = 0},
text = "5\n20",
number = 0xffffff,
}
}
function better_commands.update_hud()
local bg_width = 16
for _, player in ipairs(minetest.get_connected_players()) do
local playername = player:get_player_name()
local sidebar = better_commands.sidebars[playername]
if not sidebar then
sidebar = {}
better_commands.sidebars[playername] = sidebar
end
local team = better_commands.teams.players[playername]
local team_color, objective
if team then
team_color = better_commands.teams.teams[team].color
objective = better_commands.scoreboard.displays.colors[team_color] or better_commands.scoreboard.displays.sidebar
else
objective = better_commands.scoreboard.displays.sidebar
end
local objective_data = better_commands.scoreboard.objectives[objective]
if objective_data then
local name_text, score_text, max_width = "", "", #objective
local title = objective_data.display_name or objective
local scores = objective_data.scores
local count = 0
local sortable_scores = {}
for name, data in pairs(scores) do
count = count + 1
local display_name = better_commands.format_name(name)
local score
local format_data = objective_data.format or data.format
if format_data then
if format_data.type == "blank" then
score = ""
elseif format_data.type == "fixed" then
score = minetest.colorize("#ffffff", format_data.data)
else
score = minetest.colorize(format_data.data, tostring(data.score))
end
else
score = tostring(data.score)
end
local width = #minetest.strip_colors(display_name) + #minetest.strip_colors(score)
max_width = math.max(width + 2, max_width)
table.insert(sortable_scores, {name = display_name, score = score})
end
table.sort(sortable_scores, function(a,b)
return (a.score == b.score) and (a.name < b.name) or (tonumber(a.score) > tonumber(b.score)) end
)
for _, data in ipairs(sortable_scores) do
name_text = name_text..data.name.."\n"
score_text = score_text..data.score.."\n"
end
if not title then
if sidebar.title then
for name, id in pairs(sidebar) do
player:hud_remove(id)
sidebar[name] = nil
end
return
end
end
if not sidebar.title then
for name, def in pairs(sidebar_template) do
sidebar[name] = player:hud_add(def)
end
end
local pixel_width = max_width*13
local pixel_height = (count+2)*21
local center_x_offset = -(pixel_width/2 + 10)
player:hud_change(sidebar.title, "text", title)
player:hud_change(sidebar.title, "offset", {x = center_x_offset, y = -10})
player:hud_change(sidebar.bg, "scale", {x = pixel_width/bg_width, y = pixel_height/bg_width})
player:hud_change(sidebar.bg, "offset", {x = center_x_offset, y = -30})
player:hud_change(sidebar.names, "text", name_text)
player:hud_change(sidebar.names, "offset", {x = center_x_offset*2+20, y = 0})
player:hud_change(sidebar.scores, "text", score_text)
else
for name, id in pairs(sidebar) do
player:hud_remove(id)
sidebar[name] = nil
end
return
end
end
end
better_commands.sidebars = {}
---Gets the display name, given a name and objective
---@param name string
---@param objective? string
---@return string
function better_commands.get_display_name(name, objective)
if not objective then return name end
local objective_data = better_commands.scoreboard.objectives[objective]
if not objective_data then return name end
if not objective_data.players[name] then return name end
if objective_data.display_name then
return objective_data.display_name
elseif objective_data.players[name].display_name then
return objective_data.players[name].display_name
end
return name
end

64
API/teams.lua Normal file
View File

@ -0,0 +1,64 @@
--local bc = better_commands
---Formats a name according to team data
---@param name string
---@param player_only boolean?
---@param objective string?
---@return string
function better_commands.format_name(name, player_only, objective)
local display_name = better_commands.get_display_name(name, objective)
if player_only then
if not better_commands.players[name] then
return display_name
end
end
local team = better_commands.teams.players[name]
if not team then
return display_name
else
local team_data = better_commands.teams.teams[team]
local name_format = (team_data.name_format or "%s")
display_name = name_format:gsub("%%s", display_name)
local color = better_commands.team_colors[team_data.color or "white"]
return minetest.colorize(color, display_name)
end
end
function better_commands.format_team_name(name)
local team_data = better_commands.teams.teams[name]
if not team_data then
minetest.log("error", "Team "..name.." does not exist.")
return name
end
local color = better_commands.team_colors[team_data.color or "white"]
local result = minetest.colorize(color, team_data.display_name or name)
return result
end
better_commands.team_colors = {
dark_red = "#aa0000",
red = "#ff5555",
gold = "#ffaa00",
yellow = "#ffff55",
dark_green = "#00aa00",
green = "#55ff55",
aqua = "#55ffff",
dark_aqua = "#00aaaa",
dark_blue = "#0000aa",
blue = "#5555ff",
light_purple = "#ff55ff",
dark_purple = "#aa00aa",
white = "#ffffff",
gray = "#aaaaaa",
dark_gray = "#555555",
black = "#000000"
}
local old = minetest.format_chat_message
---@diagnostic disable-next-line: duplicate-set-field
minetest.format_chat_message = function(name, message)
name = better_commands.format_name(name)
local result = old(name, message)
return result
end

4
CHANGELOG.md Normal file
View File

@ -0,0 +1,4 @@
# Changelog
## v1.0
Initial release. Missing *lots* of commands, several `execute` subcommands, lots of scoreboard objectives, and lots of entity selectors.

30
COMMANDS/COMMANDS.lua Normal file
View File

@ -0,0 +1,30 @@
--local bc = better_commands
local command_files = {
"command_runners",
"give",
"kill",
"chat",
"execute",
"scoreboard",
"teleport",
"team",
"time",
"ability",
"playsound",
"setblock",
"summon"
}
for _, file in ipairs(command_files) do
better_commands.run_file(file, "COMMANDS")
end
better_commands.register_command("?", table.copy(minetest.registered_chatcommands.help))
--[[
-- Temporary commands for testing
better_commands.register_command("dumpscore",{func=function()minetest.log(dump(better_commands.scoreboard)) return true, nil, 1 end})
better_commands.register_command("dumpteam",{func=function()minetest.log(dump(better_commands.teams)) return true, nil, 1 end})
-- ]]

79
COMMANDS/ability.lua Normal file
View File

@ -0,0 +1,79 @@
--local bc = better_commands
local S = minetest.get_translator(minetest.get_current_modname())
better_commands.register_command("ability", {
params = "<player> <priv> [value]",
description = S("Sets <priv> of <player> to [value] (true/false). If [value] is not supplied, returns the existing value of <priv>"),
privs = {privs = true},
func = function(name, param, context)
context = better_commands.complete_context(name, context)
if not context then return false, S("Missing context"), 0 end
if not context.executor then return false, S("Missing executor"), 0 end
local split_param = better_commands.parse_params(param)
if not split_param[1] then
return false, nil, 0
end
local set = split_param[3] and split_param[3][3]:lower()
if set and set ~= "true" and set ~= "false" then
return false, S("[value] must be true or false (or missing), not '@1'", set), 0
end
local targets, err = better_commands.parse_selector(split_param[1], context, true)
if err or not targets then return false, err, 0 end
local priv = split_param[2] and split_param[2][3]
local target = targets[1]
if target.is_player and target:is_player() then
local target_name = target:get_player_name()
local privs = minetest.get_player_privs(target_name)
if not set then
if not priv then
local message = ""
local first = true
local count = 0
local sortable_privs = {}
for player_priv, value in pairs(privs) do
if value then
table.insert(sortable_privs, player_priv)
count = count + 1
end
end
table.sort(sortable_privs)
for _, player_priv in ipairs(sortable_privs) do
if not first then message = message..", " else first = false end
message = message..player_priv
end
return true, message, count
else
if minetest.registered_privileges[priv] then
return true, S("@1 = @2", priv, tostring(privs[priv])), 1
else
return false, S("Invalid privilege: @1", priv), 0
end
end
else
if not minetest.registered_privileges[priv] then
return false, S("Invalid privilege: @1", priv), 0
else
if set == "true" then
privs[priv] = true
else
privs[priv] = nil
end
minetest.set_player_privs(target_name, privs)
minetest.chat_send_player(target_name, S(
"@1 privilege @2 by @3",
priv,
set == "true" and "granted" or "revoked",
better_commands.format_name(name)
))
return true, S(
"@1 privilege @2 for @3",
set == "true" and "Granted" or "Revoked",
priv,
better_commands.format_name(name)
), 1
end
end
end
return false, S("No matching entity found"), 0
end
})

88
COMMANDS/chat.lua Normal file
View File

@ -0,0 +1,88 @@
--local bc = better_commands
local S = minetest.get_translator(minetest.get_current_modname())
better_commands.register_command("say", {
params = "<message>",
description = S("Says <message> to all players (which can include selectors such as @@a if you have the server priv)"),
privs = {shout = true},
func = function(name, param, context)
context = better_commands.complete_context(name, context)
if not context then return false, S("Missing context"), 0 end
if not context.executor then return false, S("Missing executor"), 0 end
local split_param = better_commands.parse_params(param)
if not split_param[1] then return false, nil, 0 end
local message
if context.command_block or minetest.check_player_privs(context.origin, {server = true}) then
local err
message, err = better_commands.expand_selectors(param, split_param, 1, context)
if err then return false, err, 0 end
else
message = param
end
minetest.chat_send_all(string.format("[%s] %s", better_commands.get_entity_name(context.executor), message))
return true, nil, 1
end
})
better_commands.register_command("msg", {
params = "<target> <message>",
description = S("Sends <message> privately to <target> (which can include selectors like @@a if you have the server priv)"),
privs = {shout = true},
func = function(name, param, context)
context = better_commands.complete_context(name, context)
if not context then return false, S("Missing context"), 0 end
if not context.executor then return false, S("Missing executor"), 0 end
local split_param = better_commands.parse_params(param)
if not split_param[1] and split_param[2] then
return false, nil, 0
end
local targets, err = better_commands.parse_selector(split_param[1], context)
if err or not targets then return false, err, 0 end
local target_start = S("@1 whispers to you: ", better_commands.get_entity_name(context.executor))
local message
if context.command_block or minetest.check_player_privs(context.origin, {server = true}) then
local err
message, err = better_commands.expand_selectors(param, split_param, 2, context)
if err then return false, err, 0 end
else
---@diagnostic disable-next-line: param-type-mismatch
message = param:sub(split_param[2][1], -1)
end
local count = 0
for _, target in ipairs(targets) do
if target.is_player and target:is_player() then
count = count + 1
local origin_start = S("You whisper to @1: ", better_commands.get_entity_name(target))
minetest.chat_send_player(name, origin_start..message)
minetest.chat_send_player(target:get_player_name(), target_start..message)
end
end
return true, nil, count
end
})
better_commands.register_command_alias("w", "msg")
better_commands.register_command_alias("tell", "msg")
better_commands.register_command("me", {
description = S("Broadcasts a message about yourself (which can include selectors like @@a if you have the server priv)"),
params = "<action>",
privs = {shout = true},
func = function(name, param, context)
context = better_commands.complete_context(name, context)
if not context then return false, S("Missing context"), 0 end
if not context.executor then return false, S("Missing executor"), 0 end
local split_param = better_commands.parse_params(param)
if not split_param[1] then return false, nil, 0 end
local message
if context.command_block or minetest.check_player_privs(context.origin, {server = true}) then
local err
message, err = better_commands.expand_selectors(param, split_param, 1, context)
if err then return false, err, 0 end
else
message = param
end
minetest.chat_send_all(string.format("* %s %s", better_commands.get_entity_name(context.executor), message))
return true, nil, 1
end
})

View File

@ -0,0 +1,52 @@
--local bc = better_commands
local S = minetest.get_translator(minetest.get_current_modname())
better_commands.register_command("bc", {
params = "<command data>",
description = S("Runs any Better Commands command, so Better Commands don't have to override existing commands"),
privs = {},
func = function(name, param, context)
context = better_commands.complete_context(name, context)
if not context then return false, S("Missing context"), 0 end
if not context.executor then return false, S("Missing executor"), 0 end
local command, command_param = param:match("^%/?([%S]+)%s*(.-)$")
local def = better_commands.commands[command]
if def then
local privs = context.command_block
local missing
if not privs then privs, missing = minetest.check_player_privs(name, def.privs) end
if privs then
return def.func(name, command_param, context)
else
return false, S("You don't have permission to run this command (missing privileges: @1)", table.concat(missing, ", ")), 0
end
else
return false, S("Invalid command: @1", command), 0
end
end
})
better_commands.register_command("old", {
params = "<command data>",
description = S("Runs any command that Better Commands has overridden"),
privs = {},
func = function(name, param, context)
context = better_commands.complete_context(name, context)
if not context then return false, S("Missing context"), 0 end
if not context.executor then return false, S("Missing executor"), 0 end
local command, command_param = param:match("^%/?([%S]+)%s*(.-)$")
local def = better_commands.old_commands[command]
if def then
local privs = context.command_block
local missing
if not privs then privs, missing = minetest.check_player_privs(name, def.privs) end
if privs then
return def.func(name, command_param, context)
else
return false, S("You don't have permission to run this command (missing privileges: @1)", table.concat(missing, ", ")), 0
end
else
return false, S("Invalid command: @1", command), 0
end
end
})

332
COMMANDS/execute.lua Normal file
View File

@ -0,0 +1,332 @@
--local bc = better_commands
local S = minetest.get_translator(minetest.get_current_modname())
better_commands.execute_subcommands = {
---Aligns relative to certain axes
---@param branches contextTable[]
---@param index integer
---@return boolean
---@return string?
---@nodiscard
align = function(branches, index)
local branch_data = branches[index]
local param = branches.param[branch_data.i+1]
if not param then return false, S("Missing argument for subcommand @1", "align") end
local axes = {param[3]:match("^([xyz])([xyz]?)([xyz]?)$")}
if not axes[1] then return false, S("Invalid axes: @1", param) end
for _ ,axis in pairs(axes) do
branch_data.pos[axis] = math.floor(branch_data.pos[axis])
end
branch_data.i = branch_data.i + 2
return true
end,
---Sets anchor to feet or eyes
---@param branches contextTable[]
---@param index integer
---@return boolean
---@return string?
---@nodiscard
anchored = function(branches, index)
local branch_data = branches[index]
local param = branches.param[branch_data.i+1]
if not param then return false, S("Missing argument for subcommand @1", "anchored") end
local anchor = tostring(param[3]):lower()
if anchor == "feet" or anchor == "eyes" then
branch_data.anchor = anchor
else
return false, S("Anchor must be 'feet' or 'eyes', not @1", anchor)
end
branch_data.i = branch_data.i + 2
return true
end,
---Changes executor
---@param branches contextTable[]
---@param index integer
---@return boolean|string
---@return string?
---@nodiscard
as = function(branches, index)
local branch_data = branches[index]
local param = branches.param[branch_data.i+1]
if not param then return false, S("Missing argument for subcommand @1", "as") end
if param.type ~= "selector" then
return false, S("Invalid target: @1", table.concat(param, "", 3))
end
local targets, err = better_commands.parse_selector(param, branch_data)
if err or not targets then return false, err end
if #targets > 1 then
for _, target in ipairs(targets) do
local new_branch = table.copy(branch_data)
new_branch.executor = target
new_branch.i = new_branch.i + 2
table.insert(branches, new_branch)
end
return "branched"
elseif #targets == 1 then
branch_data.executor = targets[1]
branch_data.i = branch_data.i + 2
return true
else
return "notarget"
end
end,
---Changes position
---@param branches contextTable[]
---@param index integer
---@return boolean|string
---@return string?
---@nodiscard
at = function(branches, index)
local branch_data = branches[index]
local param = branches.param[branch_data.i+1]
if not param then return false, S("Missing argument for subcommand @1", "at") end
if param.type ~= "selector" then
return false, S("Invalid target: @1", table.concat(param, "", 3))
end
local targets, err = better_commands.parse_selector(param, branch_data)
if err or not targets then return false, err end
if #targets > 1 then
for _, target in ipairs(targets) do
local new_branch = table.copy(branch_data)
new_branch.pos = target.get_pos and target:get_pos() or target
branch_data.rot = better_commands.get_entity_rotation(target) or branch_data.rot
new_branch.i = new_branch.i + 2
table.insert(branches, new_branch)
end
return "branched"
elseif #targets == 1 then
branch_data.pos = targets[1].get_pos and targets[1]:get_pos() or targets[1]
branch_data.rot = better_commands.get_entity_rotation(targets[1]) or branch_data.rot
branch_data.i = branch_data.i + 2
return true
else
return "notarget"
end
end,
---Changes rotation
---@param branches contextTable[]
---@param index integer
---@return boolean|string
---@return string?
---@nodiscard
facing = function(branches, index)
local branch_data = branches[index]
local split_param = branches.param
local i = branch_data.i
if split_param[i+1] then
if split_param[i+1][3] == "entity" and split_param[i+2].type == "selector" then
local targets, err = better_commands.parse_selector(split_param[i+2], branch_data)
if err or not targets then return false, err end
if #targets > 1 then
for _, target in ipairs(targets) do
local target_pos = target.get_pos and target:get_pos() or target
local new_branch = table.copy(branch_data)
---@diagnostic disable-next-line: param-type-mismatch
new_branch.rot = better_commands.point_at_pos(branch_data.executor, target_pos)
new_branch.i = branch_data.i + 3
end
return "branched"
elseif #targets == 1 then
local target_pos = targets[1].get_pos and targets[1]:get_pos() or targets[1]
---@diagnostic disable-next-line: param-type-mismatch
branch_data.rot = better_commands.point_at_pos(branch_data.executor, target_pos)
branch_data.i = branch_data.i + 3
return true
else
return "notarget"
end
else
local target_pos, err = better_commands.parse_pos(split_param, i+1, branch_data)
if err then
return false, err
end
---@diagnostic disable-next-line: param-type-mismatch
branch_data.rot = better_commands.point_at_pos(branch_data.executor, target_pos)
branch_data.i = branch_data.i + 4
return true
end
end
return true
end,
---Changes position
---@param branches contextTable[]
---@param index integer
---@return boolean|string
---@return string?
---@nodiscard
positioned = function(branches, index)
local branch_data = branches[index]
local param = branches.param[branch_data.i+1]
if not param then return false, S("Missing argument for subcommand @1", "positioned") end
if param[3] == "as" then
local selector = branches.param[branch_data.i+2]
if not selector or selector.type ~= "selector" then
return false, S("Invalid argument for @1", "positioned")
end
local targets, err = better_commands.parse_selector(selector, branch_data)
if err or not targets then return false, err end
if #targets > 1 then
for _, target in ipairs(targets) do
local new_branch = table.copy(branch_data)
branch_data.pos = target.get_pos and target:get_pos() or target
new_branch.i = new_branch.i + 3
table.insert(branches, new_branch)
end
return "branched"
elseif #targets == 1 then
branch_data.pos = targets[1].get_pos and targets[1]:get_pos() or targets[1]
branch_data.i = branch_data.i + 3
return true
else
return "notarget"
end
else
local pos, err = better_commands.parse_pos(branches.param, branch_data.i+1, branch_data)
if err then return false, err end
branch_data.pos = pos
branch_data.anchor = "feet"
branch_data.i = branch_data.i + 4
return true
end
end,
---Changes rotation
---@param branches contextTable[]
---@param index integer
---@return boolean|string
---@return string?
---@nodiscard
rotated = function(branches, index)
local branch_data = branches[index]
local param = branches.param[branch_data.i+1]
if not param then return false, S("Missing argument for subcommand @1", "rotated") end
if param[3] == "as" then
local selector = branches.param[branch_data.i+2]
if not selector or selector.type ~= "selector" then
return false, S("Invalid argument for rotated")
end
local targets, err = better_commands.parse_selector(selector, branch_data)
if err or not targets then return false, err end
if #targets > 1 then
for _, target in ipairs(targets) do
local new_branch = table.copy(branch_data)
branch_data.rot = better_commands.get_entity_rotation(target) or branch_data.rot
new_branch.i = new_branch.i + 3
table.insert(branches, new_branch)
end
return "branched"
elseif #targets == 1 then
branch_data.rot = better_commands.get_entity_rotation(targets[1]) or branch_data.rot
branch_data.i = branch_data.i + 3
return true
else
return "notarget"
end
else
if not (branches.param[branch_data.i+1] and branches.param[branch_data.i+2]) then
return false, S("Missing argument(s) for rotated")
end
local victim_rot = branch_data.rot
if branches.param[branch_data.i+1].type == "number" then
victim_rot.y = math.rad(tonumber(branches.param[branch_data.i+1][3]) or 0)
elseif branches.param[branch_data.i+1].type == "relative" then
victim_rot.y = victim_rot.y+math.rad(tonumber(branches.param[branch_data.i+1][3]:sub(2,-1)) or 0)
else
return false, S("Invalid argument for rotated")
end
if branches.param[branch_data.i+2].type == "number" then
victim_rot.x = math.rad(tonumber(branches.param[branch_data.i+2][3]) or 0)
elseif branches.param[branch_data.i+2].type == "relative" then
victim_rot.x = victim_rot.x+math.rad(tonumber(branches.param[branch_data.i+2][3]:sub(2,-1)) or 0)
else
return false, S("Invalid argument for rotated")
end
branch_data.rot = victim_rot
branch_data.i = branch_data.i + 3
return true
end
end,
---Runs a command
---@param branches contextTable[]
---@param index integer
---@return boolean|string
---@return string?
---@nodiscard
run = function(branches, index)
local branch_data = branches[index]
if not (
branch_data.executor
and branch_data.executor.get_pos
and branch_data.pos and type(branch_data.pos) == "table"
) then
return "notarget"
end
if not branches.param[branch_data.i+1] then return false, S("Missing command") end
local command, command_param
command, command_param = branch_data.original_command:match(
"%/?([%S]+)%s*(.-)$",
branches.param[branch_data.i+1][1]
)
while command == "bc" do
branch_data.i = branch_data.i + 1
command, command_param = branch_data.original_command:match(
"%/?([%S]+)%s*(.-)$",
branches.param[branch_data.i+1][1]
)
end
if command == "execute" then
branch_data.i = branch_data.i + 2
return true
end
local def = better_commands.commands[command]
if def and command ~= "old" and (branch_data.command_block or minetest.check_player_privs(branch_data.origin, def.privs)) then
return "done", def.func(branch_data.origin, command_param, table.copy(branch_data))
else
return false, S("Invalid command or privs: @1", command)
end
end
}
better_commands.register_command("execute", {
params = "<align|anchored|as|at|facing|positioned|rotated|run> ...",
description = S("Run any Better Command (not other commands) after changing the context"),
privs = {server = true, ban = true, privs = true},
func = function(name, param, context)
context = better_commands.complete_context(name, context)
if not context then return false, S("Missing context"), 0 end
if not context.executor then return false, S("Missing executor"), 0 end
local split_param = better_commands.parse_params(param)
if not split_param[1] then return false, nil, 0 end
local branch = 1
local branches = {param = split_param}
branches[1] = table.copy(context)
branches[1].i = 1
branches[1].original_command = param
local success_count = 0
while true do -- for each branch:
local status, message, err, count
while true do -- for each subcommand:
local cmd_index = branches[branch].i
if cmd_index > #split_param then break end
local subcmd = split_param[cmd_index][3]
if better_commands.execute_subcommands[subcmd] then
status, message, _, count = better_commands.execute_subcommands[subcmd](branches, branch)
if not status then return status, message, 0 end
if status == "branched" or status == "notarget" or status == "done" then
break
end
else
return false, S("Invalid subcommand: @1", subcmd), 0
end
end
if status == "done" then
success_count = success_count + (message and 1 or 0) -- "message" is status when done
end
if branch >= #branches then
break
else
branch = branch + 1
end
end
return true, S("Successfully executed @1 times", success_count), success_count
end
})

85
COMMANDS/give.lua Normal file
View File

@ -0,0 +1,85 @@
--local bc = better_commands
local S = minetest.get_translator(minetest.get_current_modname())
---Gets a printable name ("name * count") for an itemstack
---@param itemstack minetest.ItemStack
---@return string
local function itemstack_name(itemstack)
return string.format("%s * %s", itemstack:get_short_description(), itemstack:get_count())
end
---Handles the /give and /giveme commands
---@param receiver string The name of the receiver
---@param stack_data splitParam
---@return boolean
---@return string? err
---@return number count
---@nodiscard
-- Modified from builtin/game/chat.lua
local function handle_give_command(receiver, stack_data)
local itemstack, err = better_commands.parse_item(stack_data)
if err or not itemstack then return false, err, 0 end
if itemstack:is_empty() then
return false, S("Cannot give an empty item"), 0
elseif (not itemstack:is_known()) or (itemstack:get_name() == "unknown") then
return false, S("Cannot give an unknown item"), 0
-- Forbid giving 'ignore' due to unwanted side effects
elseif itemstack:get_name() == "ignore" then
return false, S("Giving 'ignore' is not allowed"), 0
end
local receiverref = core.get_player_by_name(receiver)
if receiverref == nil then
return false, S("@1 is not a known player", receiver), 0
end
local leftover = receiverref:get_inventory():add_item("main", itemstack)
if not leftover:is_empty() then
minetest.add_item(receiverref:get_pos(), leftover)
end
-- The actual item stack string may be different from what the "giver"
-- entered (e.g. big numbers are always interpreted as 2^16-1).
local item_name = itemstack_name(itemstack)
core.chat_send_player(receiver, S("You have been given [@1]", item_name))
return true, S("Gave [@1] to @2", item_name, better_commands.format_name(receiver)), 1
end
better_commands.register_command("give", {
params = "<target> <item> [count] [wear]",
description = S("Gives [count] of <item> to <target>"),
privs = {server = true},
func = function(name, param, context)
context = better_commands.complete_context(name, context)
if not context then return false, S("Missing context"), 0 end
if not context.executor then return false, S("Missing executor"), 0 end
local split_param = better_commands.parse_params(param)
if not (split_param[1] and split_param[2]) then
return false, nil, 0
end
local message
local targets, err = better_commands.parse_selector(split_param[1], context)
if err or not targets then return false, err, 0 end
local count = 0
for _, target in ipairs(targets) do
if target.is_player and target:is_player() then
local success, message, i = handle_give_command(target:get_player_name(), split_param[2])
if not success then return success, message, 0 end
count = count + i
end
end
if count < 1 then
return false, S("No target entity found"), 0
elseif count == 1 then
return true, message, 1
else
return true, S("Gave item to @1 players", count), count
end
end
})
better_commands.register_command("giveme", {
params = "<item> [count] [wear]",
description = S("Gives [count] of <item> to yourself"),
privs = {server = true},
func = function(name, param, context)
return better_commands.commands.give.func(name, "@s "..param, context)
end
})

56
COMMANDS/kill.lua Normal file
View File

@ -0,0 +1,56 @@
--local bc = better_commands
local S = minetest.get_translator(minetest.get_current_modname())
better_commands.register_command("kill", {
params = "[target]",
description = S("Kills [target] or self"),
privs = {server = true},
func = function(name, param, context)
context = better_commands.complete_context(name, context)
if not context then return false, S("Missing context"), 0 end
if not context.executor then return false, S("Missing executor"), 0 end
if param == "" then param = "@s" end
local split_param = better_commands.parse_params(param)
local targets, err = better_commands.parse_selector(split_param[1], context)
if err or not targets then return false, err, 0 end
local count = 0
local last
for _, target in ipairs(targets) do
if target.is_player then
if better_commands.kill_creative_players or not (target:is_player() and minetest.is_creative_enabled(target:get_player_name())) then
last = better_commands.get_entity_name(target)
better_commands.deal_damage(
---@diagnostic disable-next-line: param-type-mismatch
target,
math.max(target:get_hp(), 1000000000000), -- 1 trillion damage to make sure they die :D
{
type = "set_hp",
bypasses_totem = true,
flags = {bypasses_totem = true},
better_commands = "kill"
},
true
)
count = count + 1
end
end
end
if count < 1 then
return false, S("No matching entity found"), 0
elseif count == 1 then
return true, S("Killed @1", last), count
else
return true, S("Killed @1 entities", count), count
end
end
})
better_commands.register_command("killme", {
params = "",
description = S("Kills self"),
privs = {server = true},
func = function(name, param, context)
if param ~= "" then return false, S("Unexpected argument: @1", param), 0 end
return better_commands.commands.kill.func(name, "", context)
end
})

65
COMMANDS/playsound.lua Normal file
View File

@ -0,0 +1,65 @@
--local bc = better_commands
local S = minetest.get_translator(minetest.get_current_modname())
better_commands.register_command("playsound", {
params = "<sound> <targets|pos> [volume] [pitch] [maxDistance]",
description = "Plays a sound",
privs = {server = true},
func = function(name, param, context)
context = better_commands.complete_context(name, context)
if not context then return false, S("Missing context"), 0 end
local split_param = better_commands.parse_params(param)
if not (split_param[1] and split_param[2]) then
return false, nil, 0
end
local targets, err, next
if split_param[2].type == "selector" then
targets, err = better_commands.parse_selector(split_param[2], context)
if err or not targets then return false, err, 0 end
next = 3
else
local pos, err = better_commands.parse_pos(split_param, 2, context)
if err or not pos then return false, err, 0
end
targets = {pos}
next = 5
end
local volume, pitch, distance = 1, 1, 32
if split_param[next] then
---@diagnostic disable-next-line: cast-local-type
volume = split_param[next][3]
if volume and not tonumber(volume) then
return false, S("Must be a number, not @1", volume), 0
end
volume = tonumber(volume)
if split_param[next+1] then
---@diagnostic disable-next-line: cast-local-type
pitch = split_param[next+1][3]
if pitch and not tonumber(pitch) then
return false, S("Must be a number, not @1", pitch), 0
end
pitch = tonumber(pitch)
if split_param[next+2] then
---@diagnostic disable-next-line: cast-local-type
distance = split_param[next+2][3]
if distance and not tonumber(distance) then
return false, S("Must be a number, not @1", distance), 0
end
distance = tonumber(distance)
end
end
end
for _, target in ipairs(targets) do
local key = target.is_player and "object" or "pos"
minetest.sound_play(
split_param[1][3], {
gain = volume,
pitch = pitch,
[key] = target,
max_hear_distance = distance
}
)
end
return true, S("Sound played"), 1
end
})

607
COMMANDS/scoreboard.lua Normal file
View File

@ -0,0 +1,607 @@
--local bc = better_commands
local S = minetest.get_translator(minetest.get_current_modname())
local playerlist = minetest.get_modpath("playerlist")
local scoreboard_operators = {
["+="] = true,
["-="] = true,
["*="] = true,
["/="] = true,
["%="] = true,
["="] = true,
["<"] = true,
[">"] = true,
["><"] = true,
}
better_commands.register_command("scoreboard", {
params = "objectives|players ...",
description = "Manupulates the scoreboard. If you want more info, read the wiki.",
privs = {server = true},
func = function(name, param, context)
context = better_commands.complete_context(name, context)
if not context then return false, S("Missing context"), 0 end
local split_param = better_commands.parse_params(param)
if not (split_param[1] and split_param[2]) then
return false, S("Missing arguments"), 0
end
--minetest.log(dump(split_param))
if split_param[1][3] == "objectives" then
local subcommand = split_param[2][3]
if subcommand == "add" then
local objective_name = split_param[3] and split_param[3][3]
if not objective_name then return false, S("Missing name"), 0 end
if better_commands.scoreboard.objectives[objective_name] then
return false, S("Objective @1 already exists", objective_name), 0
end
local criterion = split_param[4] and split_param[4][3]
if not criterion then return false, S("Missing criterion"), 0 end
if not better_commands.validate_criterion(criterion) then
return false, S("Invalid criterion @1", criterion), 0
end
local display_name = (split_param[5] and split_param[5][3]) or objective_name
better_commands.scoreboard.objectives[objective_name] = {
name = objective_name,
criterion = criterion,
display_name = display_name,
scores = {}
}
return true, S("Added objective @1", objective_name), 1
elseif subcommand == "list" then
local objective_count = better_commands.count_table(better_commands.scoreboard.objectives) or 0
if objective_count < 1 then
return true, S("There are no objectives"), 1
end
local result = ""
local first = true
for _, def in pairs(better_commands.scoreboard.objectives) do
if not first then
result = result..", "
else
first = false
end
result = result..string.format("[%s]", def.display_name)
end
return true, S("There are @1 objective(s): @2", objective_count, result), objective_count
elseif subcommand == "modify" then
local objective = split_param[3] and split_param[3][3]
if not objective then return false, S("Missing objective"), 0 end
if not better_commands.scoreboard.objectives[objective] then
return false, S("Unknown scoreboard objective '@1'", objective), 0
end
local key = split_param[4] and split_param[4][3]
if not key then return false, S("Must be 'displayname' or 'numberformat'"), 0 end
local value = split_param[5] and split_param[5][3]
if key == "displayname" then
if not value then return false, S("Missing display name"), 0 end
local display_name = param:sub(split_param[5][1], -1):trim() -- Allow spaces
better_commands.scoreboard.objectives[objective].display_name = display_name
return true, S("@1 set to @2", "displayname", display_name), 1
elseif key == "numberformat" then
local format = split_param[6] and split_param[6][3]
if not value then
better_commands.scoreboard.objectives[objective].format = nil
return true, S("Cleared numberformat for @1", objective), 1
elseif value == "blank" then
better_commands.scoreboard.objectives[objective].format = {type = "blank"}
return true, S("@1 set to @2", "numberformat", "blank"), 1
elseif value == "fixed" then
if not split_param[6] then return false, S("Missing argument"), 0 end
local fixed = param:sub(split_param[6][1], -1):trim() -- Allow spaces
better_commands.scoreboard.objectives[objective].format = {type = "fixed", data = fixed}
return true, S("@1 set to @2", "numberformat", fixed), 1
elseif value == "styled" then
format = format:lower()
if better_commands.team_colors[format] then
format = better_commands.team_colors[format]
else
format = minetest.colorspec_to_colorstring(format)
if not value then
return false, S("Invalid color"), 0
end
end
better_commands.scoreboard.objectives[objective].format = {type = "color", data = format}
return true, S("@1 set to @2", "numberformat", format), 1
else
return false, S("Must be 'blank', 'fixed', or 'styled'"), 0
end
else
return false, S("Must be 'displayname' or 'numberformat'"), 0
end
elseif subcommand == "remove" then
local objective = split_param[3] and split_param[3][3]
if not objective then return false, S("Missing objective"), 0 end
if not better_commands.scoreboard.objectives[objective] then
return false, S("Unknown scoreboard objective '@1'", objective), 0
end
better_commands.scoreboard.objectives[objective] = nil
return true, S("Removed objective @1", objective), 1
elseif subcommand == "setdisplay" then
local location = split_param[3] and split_param[3][3]
if not location then return false, S("Missing argument"), 0 end
local objective = split_param[4] and split_param[4][3]
if objective and not better_commands.scoreboard.objectives[objective] then
return false, S("Unknown scoreboard objective '@1'", objective), 0
end
if location == "list" then
if not playerlist then return false, S("`list` requires the Playerlist mod"), 0 end
better_commands.scoreboard.displays["list"] = objective
elseif location == "below_name" then
better_commands.scoreboard.displays["below_name"] = objective
elseif location == "sidebar" then
better_commands.scoreboard.displays["sidebar"] = objective
else
local color = location:match("^sidebar%.(.+)")
if not color then
return false, S("Must be 'list', 'below_name', 'sidebar', or 'sidebar.<color>"), 0
elseif better_commands.team_colors[color] then
better_commands.scoreboard.displays.colors[color] = objective
else
return false, S("Invalid color: @1", color), 0
end
end
return true, S("Set display slot @1 to show objective @2", location, objective), 1
else
return false, S("Expected 'add', 'list', 'modify', 'remove', or 'setdisplay', got '@1'", subcommand), 0
end
elseif split_param[1][3] == "players" then
local subcommand = split_param[2][3]
if subcommand == "add" or subcommand == "set" or subcommand == "remove" then
local selector = split_param[3]
if not selector then return false, S("Missing target"), 0 end
local objective = split_param[4] and split_param[4][3]
if not objective then return false, ("Missing objective"), 0 end
if not better_commands.scoreboard.objectives[objective] then
return false, S("Unknown scoreboard objective '@1'", objective), 0
end
local score = tonumber(split_param[5] and split_param[5][3])
if not score then return false, S("Missing score"), 0 end
local names, err = better_commands.get_scoreboard_names(selector, context, objective)
if err or not names then return false, err, 0 end
local last
local scores = better_commands.scoreboard.objectives[objective].scores
for name in pairs(names) do
last = name
if not scores[name] then
scores[name] = {score = 0}
end
if subcommand == "add" then
scores[name].score = scores[name].score + score
elseif subcommand == "remove" then
scores[name].score = scores[name].score - score
else --if subcommand == "set"
scores[name].score = score
end
end
local name_count = better_commands.count_table(names) or 0
if name_count < 1 then
return false, S("No scores found"), 0
elseif name_count == 1 then
return true, S("Set score for @1", better_commands.format_name(last)), 1
else
return true, S("Set score for @1 entities", name_count), name_count
end
elseif subcommand == "display" then
local key = split_param[3] and split_param[3][3]
if not key then return false, S("Must be 'name' or 'numberformat'"), 0 end
if key == "name" then
local selector = split_param[4]
if not selector then return false, S("Missing target"), 0 end
local objective = split_param[5] and split_param[5][3]
if not objective then return false, ("Missing objective"), 0 end
if not better_commands.scoreboard.objectives[objective] then
return false, S("Invalid objective: @1", objective), 0
end
local display_name = nil
if split_param[6] then
display_name = param:sub(split_param[6][1], -1):trim() -- Allow spaces
end
local scores = better_commands.scoreboard.objectives[objective].scores
local names, err = better_commands.get_scoreboard_names(selector, context, objective)
if err or not names then return false, err, 0 end
local last
for name in pairs(names) do
last = name
if not scores[name] then scores[name] = {score = 0} end
scores[name].display_name = display_name
end
local name_count = better_commands.count_table(names) or 0
if name_count < 1 then
return false, S("No entities found"), 0
elseif name_count == 1 then
return true, S("Set display name of @1 to @2", better_commands.format_name(last), display_name or "default"), 1
else
return true, S("Set display name of @1 entities to @2", name_count, display_name or "default"), name_count
end
elseif key == "numberformat" then
local selector = split_param[4] and split_param[4]
if not selector then return false, S("Missing target"), 0 end
local objective = split_param[5] and split_param[5][3]
if not objective then return false, ("Missing objective"), 0 end
if not better_commands.scoreboard.objectives[objective] then
return false, S("Invalid objective: @1", objective), 0
end
local value = split_param[5] and split_param[5][3]
local format = split_param[6] and split_param[6][3]
local result, return_value
if value == nil then
result = nil
return_value = S("Cleared format for @1", objective)
elseif value == "blank" then
result = {type = "blank"}
return_value = S("@1 set to @2", "numberformat", "blank")
elseif value == "fixed" then
if not split_param[6] then return false, S("Missing argument"), 0 end
local fixed = param:sub(split_param[6][1], -1):trim() -- Allow spaces
result = {type = "fixed", data = fixed}
return_value = S("@1 set to @2", "numberformat", fixed)
elseif value == "styled" then
format = format:lower()
if better_commands.team_colors[format] then
format = better_commands.team_colors[format]
else
format = minetest.colorspec_to_colorstring(format)
if not value then
return false, S("Invalid color"), 0
end
end
result = {type = "color", data = format}
return_value = S("@1 set to @2", "numberformat", format)
else
return false, S("Must be 'blank', 'fixed', or 'styled'"), 0
end
local names, err = better_commands.get_scoreboard_names(selector, context, objective)
if err or not names then return false, err, 0 end
local scores = better_commands.scoreboard.objectives[objective].scores
local count = 0
for name in pairs(names) do
if not scores[name] then scores[name] = {score = 0} end
scores[name].format = result and table.copy(result)
count = count + 1
end
return true, return_value, count
else
return false, S("Must be 'name' or 'numberformat', not @1", key), 0
end
elseif subcommand == "enable" then
local selector = split_param[3]
if not selector then return false, S("Missing target"), 0 end
local objective = split_param[4] and split_param[4][3]
if not objective then return false, ("Missing objective"), 0 end
local objective_data = better_commands.scoreboard.objectives[objective]
if not (objective_data) then
return false, S("Invalid objective: @1", objective), 0
end
if objective_data.criterion ~= "trigger" then
return false, S("@1 is not a trigger objective", objective), 0
end
local names, err = better_commands.get_scoreboard_names(selector, context, objective)
if err or not names then return false, err, 0 end
local scores = objective_data.scores
local display_name = objective_data.display_name or objective
local last
for name in pairs(names) do
last = name
if not scores[name] then scores[name] = {score = 0} end
scores[name].enabled = true
end
local name_count = better_commands.count_table(names) or 0
if name_count < 1 then
return false, S("No players found"), 0
elseif name_count == 1 then
return true, S("Enabled trigger [@1] for @2", display_name, better_commands.format_name(last)), 1
else
return true, S("Enabled trigger [@1] for @2 players", display_name, name_count), name_count
end
elseif subcommand == "get" then
local selector = split_param[3] and split_param[3]
if not selector then return false, S("Missing target"), 0 end
local objective = split_param[4] and split_param[4][3]
if not objective then return false, ("Missing objective"), 0 end
if not better_commands.scoreboard.objectives[objective] then
return false, S("Unknown scoreboard objective '@1'", objective), 0
end
local names, err = better_commands.get_scoreboard_names(selector, context, objective, true)
if err or not names then return false, err, 0 end
local name = names[1]
if name then
local score = better_commands.scoreboard.objectives[objective].scores[name]
local display_name = better_commands.scoreboard.objectives[objective].display_name or objective
return true, S("@1 has @2 [@3]", better_commands.format_name(name), score, display_name), 1
else
return false, S("@1 does not have a score for @2", better_commands.format(name), objective), 1
end
elseif subcommand == "list" then
local selector = split_param[3]
if not selector then
local results = {}
for _, data in pairs(better_commands.scoreboard.objectives) do
for name in pairs(data.scores) do
results[name] = true
end
end
local result_string = ""
local first = true
local result_count = 0
for result in pairs(results) do
if not first then
result_string = result_string..", "
else
first = false
end
result_string = result_string..better_commands.format_name(result)
result_count = result_count + 1
end
if result_count < 1 then
return true, S("There are no tracked players"), 1
end
return true, S("There are @1 tracked player(s): @2", result_count, result_string), result_count
else
local names, err = better_commands.get_scoreboard_names(selector, context, nil, true)
if err or not names then return false, err, 0 end
local name = names[1]
local results = {}
for _, data in pairs(better_commands.scoreboard.objectives) do
for score_name, score_data in pairs(data.scores) do
if score_name == name then
results[data.display_name] = score_data.score
end
end
end
local result_string = ""
local result_count = 0
for objective, score in pairs(results) do
result_string = result_string..string.format("\n[%s]: %s", objective, score)
result_count = result_count + 1
end
if result_count < 1 then
return true, S("@1 has no scores", better_commands.format_name(name)), 0
end
return true, S("@1 has @2 score(s): @3", better_commands.format_name(name), result_count, result_string), 1
end
elseif subcommand == "operation" then
local source_selector = split_param[3]
if not source_selector then return false, S("Missing source selector"), 0 end
local source_objective = split_param[4] and split_param[4][3]
if not source_objective then return false, S("Missing source objective"), 0 end
if not better_commands.scoreboard.objectives[source_objective] then
return false, S("Invalid source objective"), 0
end
local operator = split_param[5] and split_param[5][3]
if not operator then return false, S("Missing operator"), 0 end
if not scoreboard_operators[operator] then
return false, S("Invalid operator: @1", operator), 0
end
local target_selector = split_param[6]
if not target_selector then return false, S("Missing target selector"), 0 end
local target_objective = split_param[7] and split_param[7][3]
if not target_objective then return false, S("Missing target objective"), 0 end
if not better_commands.scoreboard.objectives[target_objective] then
return false, S("Invalid target objective"), 0
end
local sources, err = better_commands.get_scoreboard_names(source_selector, context)
if err or not sources then return false, err, 0 end
local targets, err = better_commands.get_scoreboard_names(target_selector, context)
local source_scores = better_commands.scoreboard.objectives[source_objective].scores
local target_scores = better_commands.scoreboard.objectives[target_objective].scores
if err or not targets then return false, err, 0 end
local change_count, score_count = 0, 0
local last_source, last_target, op_string, preposition
local swap = false
for target in pairs(targets) do
score_count = score_count + 1
if not target_scores[target] then
target_scores[target] = {score = 0}
end
for source in pairs(sources) do
last_source, last_target = source, target
change_count = change_count + 1
if not source_scores[source] then
source_scores[source] = {score = 0}
end
if operator == "+=" then
target_scores[target].score = target_scores[target].score + source_scores[source].score
op_string, preposition = "Added", "to"
elseif operator == "-=" then
target_scores[target].score = target_scores[target].score - source_scores[source].score
op_string, preposition = "Subtracted", "from"
elseif operator == "*=" then
target_scores[target].score = target_scores[target].score * source_scores[source].score
op_string, preposition, swap = "Multiplied", "by", true
elseif operator == "/=" then
target_scores[target].score = target_scores[target].score / source_scores[source].score
op_string, preposition, swap = "Divided", "by", true
elseif operator == "%=" then
target_scores[target].score = target_scores[target].score % source_scores[source].score
op_string, preposition, swap = "Modulo-ed (?)", "and", true
elseif operator == "=" then
target_scores[target].score = source_scores[source].score
op_string, preposition, swap = "Set", "to", true
elseif operator == "<" then
if source_scores[source].score < target_scores[target].score then
target_scores[target].score = source_scores[source].score
op_string, preposition, swap = "Set", "to", true
end
elseif operator == ">" then
if source_scores[source].score > target_scores[target].score then
target_scores[target].score = source_scores[source].score
op_string, preposition, swap = "Set", "to", true
end
else --if operator == "><" then
source_scores[source].score, target_scores[target].score
= target_scores[target].score, source_scores[source].score
op_string, preposition, swap = "Set", "to", true
end
end
end
if change_count < 1 then
return false, S("No matching entity found"), 0
elseif change_count == 1 then
return true, S(
"@1 [@2] score of @3 @4 [@5] score of @6", -- a bit unnecessary, perhaps.
op_string,
swap and better_commands.scoreboard.objectives[target_objective].display_name or better_commands.scoreboard.objectives[source_objective].display_name,
swap and better_commands.format_name(last_target) or better_commands.format_name(last_source),
preposition,
swap and better_commands.scoreboard.objectives[source_objective].display_name or better_commands.scoreboard.objectives[target_objective].display_name,
swap and better_commands.format_name(last_source) or better_commands.format_name(last_target)
), 1
else
return true, S("Changed @1 scores (@2 total operations)", score_count, change_count), score_count
end
elseif subcommand == "random" then
local selector = split_param[3]
if not selector then return false, S("Missing selector"), 0 end
local objective = split_param[4] and split_param[4][3]
if not objective then return false, S("Missing objective"), 0 end
if not better_commands.scoreboard.objectives[objective] then
return false, S("Invalid objective"), 0
end
local min = split_param[5] and split_param[5][3]
if not min then return false, S("Missing min"), 0 end
---@diagnostic disable-next-line: cast-local-type
min = tonumber(min)
if not min then return false, S("Must be a number"), 0 end
local max = split_param[6] and split_param[6][3]
if not max then return false, S("Missing max"), 0 end
max = tonumber(max)
if not max then return false, S("Must be a number"), 0 end
local names, err = better_commands.get_scoreboard_names(selector, context)
if err or not names then return false, err, 0 end
local scores = better_commands.scoreboard.objectives[objective].scores
local count = 0
local last
for name in pairs(names) do
count = count + 1
last = name
if not scores[name] then scores[name] = {} end
scores[name].score = math.random(min, max)
end
if count < 1 then
return false, S("No target entities found"), 0
elseif count == 1 then
return true, S("Randomized score for @1", better_commands.format_name(last)), 1
else
return true, S("Randomized @2 scores", count), count
end
elseif subcommand == "reset" then
local selector = split_param[3]
if not selector then return false, S("Missing selector"), 0 end
local objective = split_param[4] and split_param[4][3]
if objective and not better_commands.scoreboard.objectives[objective] then
return false, S("Invalid objective"), 0
end
local names, err = better_commands.get_scoreboard_names(selector, context)
if err or not names then return false, err, 0 end
local count = 0
local last
for name in pairs(names) do
count = count + 1
last = name
if objective then
better_commands.scoreboard.objectives[objective].scores[name] = nil
else
for _, objective in pairs(better_commands.scoreboard.objectives) do
objective.scores[name] = nil
end
end
end
if count < 1 then
return true, S("No target entities found"), 0
elseif count == 1 then
return true, S("Reset score for @1", better_commands.format_name(last)), 1
else
return true, S("Reset @2 scores", count), 1
end
elseif subcommand == "test" then
local selector = split_param[3]
if not selector then return false, S("Missing selector"), 0 end
local objective = split_param[4] and split_param[4][3]
if not objective then return false, S("Missing objective"), 0 end
if not better_commands.scoreboard.objectives[objective] then
return false, S("Invalid objective"), 0
end
local min = split_param[5] and split_param[5][3]
if not min then return false, S("Missing min"), 0 end
if min == "*" then min = -99999999999999 end -- the minimum value before losing precision
min = tonumber(min)
if not min then return false, S("Must be a number"), 0 end
local max = split_param[6] and split_param[6][3]
if not max then return false, S("Missing max"), 0 end
if max == "*" then max = 100000000000000 end -- the maximum value before losing precision
max = tonumber(max)
if not max then return false, S("Must be a number"), 0 end
local names, err = better_commands.get_scoreboard_names(selector, context, objective, true)
if err or not names then return false, err, 0 end
local scoreboard_name = names[1]
local scores = better_commands.scoreboard.objectives[objective].scores
if not scores[scoreboard_name] then
return false, S("Player @1 has no scores recorded", better_commands.format_name(scoreboard_name)), 0
elseif scores[scoreboard_name].score >= min and scores[scoreboard_name].score <= max then
return true, S("Score @1 is in range @2 to @3", scores[scoreboard_name].score, min, max), 1
else
return false, S("Score @1 is NOT in range @2 to @3", scores[scoreboard_name].score, min, max), 0
end
else
return false, S("Expected 'add', 'display', 'enable', 'get', 'list', 'operation', 'random', 'reset', 'set', or 'test', got @1", subcommand), 0
end
else
return false, nil, 0
end
end
})
better_commands.register_command("trigger", {
description = S("Allows players to set their own scores in certain conditions"),
privs = {},
param = "<objective> [add|set <value>]",
func = function (name, param, context)
context = better_commands.complete_context(name, context)
if not context then return false, S("Missing context"), 0 end
if not context.executor then return false, S("Missing executor"), 0 end
if not (context.executor.is_player and context.executor:is_player()) then
return false, S("/trigger can only be used by players"), 0
end
local player_name = context.executor:get_player_name()
local split_param = better_commands.parse_params(param)
local objective = split_param[1] and split_param[1][3]
if not objective then return false, nil, 0 end
local objective_data = better_commands.scoreboard.objectives[objective]
if not objective_data then
return false, S("Unknown scoreboard objective '@1'", objective), 0
end
if objective_data.criterion ~= "trigger" then
return false, S("You can only trigger objectives that are 'trigger' type"), 0
end
local scores = objective_data.scores[player_name]
if not scores then
return false, S("You cannot trigger this objective yet"), 0
end
if not scores.enabled then
return false, S("You cannot trigger this objective yet"), 0
end
local subcommand = split_param[2] and split_param[2][3]
local display_name = objective_data.display_name or objective
if not subcommand then
scores.score = scores.score + 1
scores.enabled = false
return true, S("Triggered [@1]", display_name), scores.score
else
local value = split_param[3] and split_param[3][3]
if not value then return false, S("Missing value"), 0 end
if not tonumber(value) then return false, S("Value must be a number"), 0 end
if subcommand == "add" then
scores.score = scores.score + tonumber(value)
scores.enabled = false
return true, S("Triggered [@1] (added @2 to value)", display_name, value), scores.score
elseif subcommand == "set" then
scores.score = tonumber(value)
scores.enabled = false
return true, S("Triggered [@1] (set value to @2)", display_name, value), scores.score
else
return false, S("Expected 'add' or 'set', got @1", subcommand), 0
end
end
end
})

43
COMMANDS/setblock.lua Normal file
View File

@ -0,0 +1,43 @@
--local bc = better_commands
local S = minetest.get_translator(minetest.get_current_modname())
better_commands.register_command("setblock", {
params = "<pos> <block> [keep|replace]",
description = S("Places <block> at <pos>. If keep, only replace air"),
func = function(name, param, context)
context = better_commands.complete_context(name, context)
if not context then return false, S("Missing context"), 0 end
if not context.executor then return false, S("Missing executor"), 0 end
local split_param = better_commands.parse_params(param)
if not split_param[1] and split_param[2] and split_param[3] and split_param[4] then
return false, nil, 0
end
local keep
if split_param[5] then
keep = split_param[5][3]:lower()
if keep ~= "keep" and keep ~= "replace" then
return false, S("Last argument ust be either 'replace' (default), 'keep', or missing, not @1", keep), 0
end
end
local pos, err = better_commands.parse_pos(split_param, 1, context)
if err or not pos then return false, err, 0 end
local node, meta, err = better_commands.parse_node(split_param[4])
if err or not node then return false, err, 0 end
if keep == "keep" and minetest.get_node(pos).name ~= "air" then
return false, S("Position is not empty"), 0
end
minetest.set_node(pos, node)
if meta and meta ~= {} then
local node_meta = minetest.get_meta(pos)
for key, value in pairs(meta) do
node_meta:set_string(key, value)
end
end
return true, S("Node set"), 1
end
})
better_commands.register_command_alias("setnode", "setblock")

34
COMMANDS/summon.lua Normal file
View File

@ -0,0 +1,34 @@
local S = minetest.get_translator(minetest.get_current_modname())
--local bc = better_commands
better_commands.register_command("summon", {
description = S("Summons an entity"),
params = "<entity> [pos] [ (<yRot> [xRot]) | (facing <entity>) ] [nametag])",
privs = {server = true},
func = function(name, param, context)
context = better_commands.complete_context(name, context)
if not context then return false, S("Missing context"), 0 end
if not context.executor then return false, S("Missing executor"), 0 end
local split_param = better_commands.parse_params(param)
local entity = split_param[1]
if not entity then return false, S("Missing entity"), 0 end
local checked_entity = better_commands.entity_from_alias(entity[3])
if not checked_entity then return false, S("Invalid entity: @1", entity[3]), 0 end
local summoned
if split_param[2] then
local pos, err = better_commands.parse_pos(split_param, 2, context)
if err or not pos then return false, err, 0 end
summoned = minetest.add_entity(pos, checked_entity, entity[4])
if not summoned then return false, S("Could not summon @1", entity[3]), 0 end
if split_param[5] then
local victim_rot, err = better_commands.get_tp_rot(context, summoned, split_param, 5)
if err or not victim_rot then return false, err, 0 end
better_commands.set_entity_rotation(summoned, victim_rot)
end
else
summoned = minetest.add_entity(context.pos, checked_entity, entity[4])
if not summoned then return false, S("Could not summon @1", entity[3]), 0 end
end
return true, S("Summoned @1", better_commands.get_entity_name(summoned)), 1
end
})

196
COMMANDS/team.lua Normal file
View File

@ -0,0 +1,196 @@
local bc = better_commands
local S = minetest.get_translator(minetest.get_current_modname())
better_commands.register_command("team", {
params = "add|empty|join|leave|list|modify|remove ...",
description = "Controls teams",
privs = {server = true},
func = function (name, param, context)
context = better_commands.complete_context(name, context)
if not context then return false, S("Missing context"), 0 end
if not context.executor then return false, S("Missing executor"), 0 end
local split_param, err = better_commands.parse_params(param)
if err then return false, err, 0 end
if not split_param[1] then return false, S("Missing subcommand"), 0 end
local subcommand = split_param[1] and split_param[1][3]
if subcommand == "add" then
local team_name = split_param[2] and split_param[2][3]
if not team_name then return false, S("Missing team name"), 0 end
if better_commands.teams.teams[team_name] then
return false, S("Team @1 already exists", team_name), 0
end
if team_name:find("[^%w_]") then
return false, S("Invalid team name @1: Can only contain letters, numbers, and underscores", team_name), 0
end
local display_name = split_param[3] and split_param[3][3]
if not display_name then display_name = team_name end
better_commands.teams.teams[team_name] = {name = team_name, display_name = display_name}
return true, S("Added team @1", team_name), 1
elseif subcommand == "empty" or subcommand == "remove" then
local team_name = split_param[2] and split_param[2][3]
if not team_name then return false, S("Missing team name"), 0 end
if better_commands.teams.teams[team_name] then
local display_name = better_commands.format_team_name(team_name)
if subcommand == "remove" then
better_commands.teams.teams[team_name] = nil
end
for player_name, player_team in pairs(better_commands.teams.players) do
if player_team == team_name then
better_commands.teams.players[player_name] = nil
end
end
if subcommand == "remove" then
return true, S("Removed team [@1]", display_name), 1
end
return true, S("Removed all players from team [@1]", display_name), 1
else
return false, S("Team @1 does not exist", team_name), 0
end
elseif subcommand == "join" then
local team_name = split_param[2] and split_param[2][3]
if not team_name then return false, S("Missing team name"), 0 end
if not better_commands.teams.teams[team_name] then
return false, S("Team @1 does not exist", team_name), 0
end
local selector = split_param[3]
if not selector then
if context.executor.is_player and context.executor:is_player() then
better_commands.teams.players[context.executor:get_player_name()] = team_name
return true, S("Joined team [@1]", better_commands.format_team_name(team_name)), 1
end
else
local count = 0
local last
local names, err = better_commands.get_scoreboard_names(selector, context)
if err or not names then return false, err, 0 end
for name in pairs(names) do
if count < 1 then last = better_commands.format_name(name) end
count = count + 1
better_commands.teams.players[name] = team_name
end
if count < 1 then
return false, S("No target entities found"), 0
elseif count == 1 then
return true, S("Added @1 to team [@2]", better_commands.format_name(last), better_commands.format_team_name(team_name)), 1
else
return true, S("Added @1 entities to [@2]", count, better_commands.format_team_name(team_name)), 1
end
end
elseif subcommand == "leave" then
local selector = split_param[2]
local count = 0
local last
if not selector then
if context.executor.is_player and context.executor:is_player() then
last = context.executor:get_player_name()
count = 1
better_commands.teams.players[last] = nil
else
return false, S("Non-players cannot be on a team"), 0
end
else
local names, err = better_commands.get_scoreboard_names(selector, context)
if err or not names then return false, err, 0 end
for _, name in ipairs(names) do
if better_commands.teams.players[name] then
count = count + 1
last = name
better_commands.teams.players[name] = nil
end
end
end
if count < 1 then
return false, S("No target entities found"), 0
elseif count == 1 then
return true, S("Removed @1 from any team", better_commands.format_name(last)), 1
else
return true, S("Removed @1 from any team", count), 1
end
elseif subcommand == "list" then
local team_name = split_param[2] and split_param[2][3]
if not team_name then
local count = 0
local result = ""
local comma
for team, team_data in pairs(better_commands.teams.teams) do
count = count + 1
local color = team_data.color or "white"
color = better_commands.team_colors[color] or color
if comma then result = result..", " else comma = true end
result = result..string.format("[%s]", minetest.colorize(color, team_data.display_name or team))
end
if count > 0 then
return true, S("There are @1 team(s): @2", count, result), 1
else
return true, S("There are no teams"), 1
end
elseif better_commands.teams.teams[team_name] then
local count = 0
local result = ""
local comma
local display_name = better_commands.format_team_name(team_name)
for name, team in pairs(better_commands.teams.players) do
if team == team_name then
count = count + 1
local formatted_name = better_commands.format_name(name)
if comma then result = result..", " else comma = true end
result = minetest.colorize("#00ff00", minetest.strip_colors(formatted_name)) -- not sure why ACOVG makes it green
end
end
if count > 0 then
return true, S("Team [@1] has @2 member(s): @3", display_name, count, result), count
else
return true, S("There are no members on team [@1]", display_name), 1
end
else
return false, S("Team [@1] does not exist", team_name), 0
end
elseif subcommand == "modify" then
local team_name = split_param[2] and split_param[2][3]
if not team_name then return false, S("Team name is required"), 0 end
local team_data = better_commands.teams.teams[team_name]
if not team_data then return false, S("Unknown team '@1'", team_name), 0 end
local key = split_param[3] and split_param[3][3]
if not key then return false, S("Missing key"), 0 end
local value = split_param[4] and split_param[4][3]
if key == "color" then
if value then
if not better_commands.team_colors[value] then return false, S("Invalid color: @1", value), 0 end
team_data.color = value
return true, S("Set color of team [@1] to @2", better_commands.format_team_name(team_name), value), 1
else
team_data.color = nil
return true, S("Reset color of team [@1]", better_commands.format_team_name(team_name)), 1
end
elseif key == "displayName" then
if value then
team_data.display_name = value
return true, S("Set display name of team [@1] to @2", better_commands.format_team_name(team_name), value), 1
else
team_data.display_name = team_name
return true, S("Reset display name of team [@1]", better_commands.format_team_name(team_name)), 1
end
elseif key == "friendlyFire" then
if value == "true" then
team_data.pvp = true
elseif value == "false" then
team_data.pvp = false
else
return false, S("Value must be 'true' or 'false', not @1", value), 0
end
return true, S("Set friendly fire for team [@1] to @2", better_commands.format_team_name(team_name), value), 1
elseif key == "nameFormat" then
if not split_param[4] then
team_data.name_format = nil
return true, S("Reset name format for team [@1]", better_commands.format_team_name(team_name)), 1
end
local name_format = param:sub(split_param[4][1], -1)
team_data.name_format = name_format
return true, S("Set name format for team [@1] to @2", better_commands.format_team_name(team_name), value), 1
else
return false, S("Value must be 'color', 'displayName', 'friendlyFire', or 'nameFormat'"), 0
end
end
return false, S("Must be 'add', 'empty', 'join', 'leave', 'list', 'modify', or 'remove', not @1", subcommand), 0
end
})

129
COMMANDS/teleport.lua Normal file
View File

@ -0,0 +1,129 @@
--local bc = better_commands
local S = minetest.get_translator(minetest.get_current_modname())
-- some duplicate code
better_commands.register_command("teleport", {
params = "[entity/ies] <location/entity> ([yaw] [pitch] | [facing <location/entity>])",
description = S("Teleports and rotates things"),
privs = {server = true},
func = function(name, param, context)
context = better_commands.complete_context(name, context)
if not context then return false, S("Missing context"), 0 end
if not context.executor then return false, S("Missing executor"), 0 end
local split_param = better_commands.parse_params(param)
if not split_param[1] then return false, nil, 0 end
if split_param[1].type == "selector" then
if not split_param[2] then
if not context.executor.is_player then
return false, S("Command blocks can't teleport (although I did consider making it possible)"), 0
end
local targets, err = better_commands.parse_selector(split_param[1], context, true)
if err or not targets then return false, err, 0 end
local target_pos = targets[1].is_player and targets[1]:get_pos() or targets[1]
context.executor:set_pos(target_pos)
context.executor:add_velocity(-context.executor:get_velocity())
local rotation = better_commands.get_entity_rotation(targets[1])
better_commands.set_entity_rotation(context.executor, rotation)
return true, S("Teleported @1 to @2", better_commands.get_entity_name(context.executor), better_commands.get_entity_name(targets[1])), 1
elseif split_param[2].type == "selector" then
if not context.executor.is_player and split_param[1][3] == "@s" then
return false, S("Command blocks can't teleport (although I did consider making it possible)"), 0
end
local victims, err = better_commands.parse_selector(split_param[1], context)
if err or not victims then return false, err, 0 end
if #victims == 0 then
return false, S("No matching entities found"), 0
end
local targets, err = better_commands.parse_selector(split_param[2], context, true)
if err or not targets then return false, err, 0 end
local target_pos = targets[1].is_player and targets[1]:get_pos() or targets[1]
local count = 0
local last
for _, victim in ipairs(victims) do
if victim.is_player then
count = count + 1
last = better_commands.get_entity_name(victim)
victim:set_pos(target_pos)
victim:add_velocity(-victim:get_velocity())
local rotation = better_commands.get_entity_rotation(targets[1])
better_commands.set_entity_rotation(victim, rotation)
end
end
if count < 1 then
return false, S("No entities found"), 0
elseif count == 1 then
return true, S(
"Teleported @1 to @2",
last,
better_commands.get_entity_name(targets[1])
),
1
else
return true, S(
"Teleported @1 entities to @2",
count,
better_commands.get_entity_name(targets[1])
),
count
end
elseif split_param[2].type == "number" or split_param[2].type == "relative" or split_param[2].type == "look_relative" then
if not context.executor.is_player and split_param[1][3] == "@s" then
return false, S("Command blocks can't teleport (although I did consider making it possible)"), 0
end
local victims, err = better_commands.parse_selector(split_param[1], context)
if err or not victims then return false, err, 0 end
local target_pos, err = better_commands.parse_pos(split_param, 2, context)
if err then return false, err, 0 end
local count = 0
local last
for _, victim in ipairs(victims) do
if victim.is_player then
count = count+1
last = better_commands.get_entity_name(victim)
victim:set_pos(target_pos)
if not (split_param[2].type == "look_relative"
or split_param[2].type == "relative"
or split_param[3].type == "relative"
or split_param[4].type == "relative") then
victim:add_velocity(-victim:get_velocity())
end
local victim_rot, err = better_commands.get_tp_rot(context, victim, split_param, 5)
if err then return false, err, 0 end
if victim_rot then
better_commands.set_entity_rotation(victim, victim_rot)
end
end
end
if count < 1 then
return false, S("No entities found"), 0
elseif count == 1 then
return true, S("Teleported @1 to @2", last, minetest.pos_to_string(target_pos, 1)), 1
else
return true, S("Teleported @1 entities to @2", count, minetest.pos_to_string(target_pos, 1)), count
end
end
elseif split_param[1].type == "number" or split_param[1].type == "relative" or split_param[1].type == "look_relative" then
if not context.executor.is_player and split_param[1][3] == "@s" then
return false, S("Command blocks can't teleport (although I did consider making it possible)"), 0
end
local target_pos, err = better_commands.parse_pos(split_param, 1, context)
if err then
return false, err, 0
end
context.executor:set_pos(target_pos)
if not (split_param[1].type == "look_relative"
or split_param[1].type == "relative"
or split_param[2].type == "relative"
or split_param[3].type == "relative") then
context.executor:add_velocity(-context.executor:get_velocity())
end
local victim_rot, err = better_commands.get_tp_rot(context, context.executor, split_param, 4)
if err or not victim_rot then return false, err, 0 end
better_commands.set_entity_rotation(context.executor, victim_rot)
return true, S("Teleported @1 to @2", better_commands.get_entity_name(context.executor), minetest.pos_to_string(target_pos, 1)), 1
end
return false, nil, 0
end
})
better_commands.register_command_alias("tp", "teleport")

51
COMMANDS/time.lua Normal file
View File

@ -0,0 +1,51 @@
--local bc = better_commands
local S = minetest.get_translator(minetest.get_current_modname())
better_commands.times = {
day = 7000/24000,
night = 19000/24000,
noon = 12000/24000,
midnight = 0/24000,
sunrise = 5000/24000,
sunset = 18000/24000
}
better_commands.register_command("time", {
params = "(add <time>)|(set <time>)|(query daytime|gametime|day)",
description = S("Sets or gets the time"),
privs = {settime = true, server = true},
func = function(name, param, context)
context = better_commands.complete_context(name, context)
if not context then return false, S("Missing context"), 0 end
if not context.executor then return false, S("Missing executor"), 0 end
local split_param = better_commands.parse_params(param)
if not (split_param[1] and split_param[2]) then return false, nil, 0 end
local action = split_param[1][3]:lower()
local time = split_param[2][3]:lower()
if action == "add" then
local new_time, err = better_commands.parse_time_string(time)
if err then return false, err, 0 end
minetest.set_timeofday(new_time)
return true, S("Time set"), 1
elseif action == "query" then
if time == "daytime" then
if better_commands.mc_time then
return true, S("Current time: @1", math.floor(minetest.get_timeofday()*24000+18000) % 24000), 1
else
return true, S("Current time: @1", math.floor(minetest.get_timeofday()*24000)), 1
end
elseif time == "gametime" then
return true, S("Time since world creation: @1", (minetest.get_gametime() or 0)*24000), 1
elseif time == "day" then
return true, S("Day count: @1", minetest.get_day_count()), 1
end
return false, S("Must be 'daytime', 'gametime', or 'day', got @1", time), 0
elseif action == "set" then
local new_time, err = better_commands.parse_time_string(time, true)
if err then return false, err, 0 end
minetest.set_timeofday(new_time)
return true, S("Time set"), 1
end
return false, S("Must be 'add', 'set', or 'query'"), 0
end
})

21
LICENSE.txt Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2024 ThePython10110
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.

207
README.md Normal file
View File

@ -0,0 +1,207 @@
# Better Commands
Adds commands and syntax from a certain other voxel game (such as `/kill @e[type=mobs_mc:zombie, distance = 2..]`) to Minetest. For compatible command blocks, use my [Better Command Blocks](https://content.minetest.net/packages/ThePython/better_command_blocks/) mod. I'm basically copying them from a certain other voxel game (whose name will not be mentioned), hereafter referred to as ACOVG. I may eventually make a wiki to explain the differences.
### PLEASE help with bug reports and PR's
This is kind of a huge project. ACOVG's commands are complicated. If you would like, you can also help translate it on Weblate.
## Current command list:
* `/bc`\*: Allows players to run any command added by this mod even if its name matches the name of an existing command (for example, `/bc give @a default:dirt` or even `/bc bc /bc /bc bc give @a default:dirt`)
* `/old`\* Basically the opposite of `bc`, running any command overridden by commands from this mod.
* `/?`: Alias for `/help`
* `/ability <player> <priv> [true/false]`: Shows or sets `<priv>` of `<player>`.
* `/execute align|anchored|as|at|facing|positioned|rotated|run ...`: Runs other Better Commands (not other commands) after changing the context. If you want more information, check ACOVG's wiki because I'm not explaining it all here. Some arguments will not be added, others will be but haven't yet.
* `/give <player> <item>`: Gives `<item>` to `<player>`
* `/giveme <item>`\*: Equivalent to `/give @s <item>`
* `/kill [target]`: Kills entities (or self if left empty)
* `/killme`\*: Equivalent to `/kill @s`
* `/me <message>`: Broadcasts a message about yourself
* `/msg`: Alias for `/tell`
* `/say <message>`: Sends a message to all connected players (supports entity selectors in `<message>`)
* `/summon <entity> [pos] [rotation]` Summons an entity
* `/scoreboard objectives|players ...`: Manipulates the scoreboard
* `/setblock <pos> <node> [destroy|keep|replace]`: Places nodes (supports metadata/param1/param2).
* `/setnode`: Alias for `/setblock`
* `/team add|empty|join|leave|list|modify|remove ...`: Manipulates teams.
* `/teleport [too many argument combinations]`: Sets entities' position and rotation
* `/tell <player> <message>`: Sends a message to specific players (supports entity selectors in `<message>`)
* `/trigger <objective> [add|set <value>]`: Allows players to set their own scores in controlled ways
* `/tp`: Alias for `/teleport`
* `/w`: Alias for `/tell`
\* Not in ACOVG
## Entity selectors
Everywhere you would normally enter a player name, you can use an entity selector instead. Entity selectors let you choose multiple entities and narrow down exactly which ones you want to include.
There are 5 selectors:
* `@s`: Self (the player running the command)
* `@a`: All players
* `@e`: All entities
* `@r`: Random player
* `@p`: Nearest player
`@r` and `@p` can also select multiple players or other entities if using the `type` or `limit`/`c` **arguments** (explained below).
### Selector arguments
Selectors support various arguments, which allow you to select more specific entities. To add arguments to a selector, put them in `[square brackets]` like this:
```
@e[type=mobs_mc:zombie,name=Bob]
```
You can include spaces if you want (although this many spaces seems a bit excessive):
```
@e [ type = mobs_mc:zombie , name = Bob ]
```
This selector selects all MCLA/VL zombies named Bob.
All arguments must be satisfied for an entity to be selected.
`@s` ignores all arguments, unlike in ACOVG.
Here is the current list of arguments:
* `x`/`y`/`z`: Sets the position for the `distance`/`rm`/`r` arguments. If one or more are left out, they stay the same.
* `distance`: Distance from where the command was run. This supports ranges (described below).
* `rm`/`r`: Identical to `distance=<rm>..<r>` (this is slightly different from ACOVG's usage).
* `name`: The name of the entity
* `type`: The entity ID (for example `mobs_mc:zombie`).
* `sort`: The method for sorting entities. Can be `arbitrary` (default for `@a` and `@e`), `nearest` (default for `@p`), `furthest`, or `random` (default for `@r`).
* `limit`/`c`: The maximum number of entites to match. `limit` and `c` do exactly the same thing, and only one can be included.
#### Entity aliases
Some entities have aliases. For example, you can type use `@e[type=zombie]` to select all zombies, instead of having to use `@e[type=mobs_mc:zombie]`. Aliases currently exist for items (`item` instead of `__builtin:item`), falling nodes (`falling_node` or `falling_block`), and mobs from the following mods:
* Animalia
* Dmobs
* Draconis
* Wilhelmines Living Nether
* Mobs Animal
* balrug (flux's fork)
* Water Mobs (`mobs_crocs`, `mobs_fish`, `mobs_jellyfish`, `mobs_sharks`, `mobs_turtles`)
* Mob Horse
* Mob Mese Monster Classic
* Mobs Monster
* Mobs Skeletons
* VoxeLibre and Mineclonia
* Not So Simple Mobs
You can add or change aliases in `entity_aliases.lua`.
#### Number ranges
Some arguments (currently just `distance` at the moment) support number ranges. These are basically `min..max` (you don't need both). Everywhere a range is accepted, a normal number will also be accepted.
Examples of ranges:
* `1..1`: Matches exactly 1
* `1..2`: Matches any number between 1 and 2 (inclusive)
* `1..`: Matches any number greater than or equal to 1
* `..-1.5`: Matches any number less than or equal to -1.5
* `1..-1`: Matches no numbers (since it would have to be greater than 1 *and* less than -1, which is impossible).
#### Excluding with arguments
Some arguments (such as `name` and `type`) allow you to prefix the value with `!`. This means that it will match anything *except* the entered value. For example, since `@e[type=player]` matches all players, `@e[type=!player]` matches all entities that are *not* players. Arguments testing for equality cannot be duplicated, while arguments testing for inequality can. In other words, you can have as many `type=!<something>` as you want but only one `type=<something>`.
## Known Issues:
1. I can't figure out how to do quotes or escape characters. This means that you cannot do things like `/kill @e[name="Trailing space "]` or have `]` in any part of entity/item/node data.
2. `/tp` does not support the `checkForBlocks` argument in one version of ACOVG. This *might* change in the future.
3. Only entities that use `luaentity.nametag` or `luaentity._nametag` for nametags (and players, of course) are supported by the `name` selector argument. This includes all mobs from MCLA/VL and Mobs Redo, but potentially not others.
4. `/setblock` only supports `replace` or `keep`, not destroy, and only places nodes using `set_node`. Some nodes may not act right since they weren't placed by a player. You could, in theory, look at the node's code and set its metadata...
5. `/time` does not properly add to the day count.
6. Only players (not other entities) are supported by scoreboards, teams, and entity tags, since other entities don't have UUIDs. This *might* change.
7. Except in MCLA/VL, the `playerKillCount` and `killed_by`, `teamkill`, and `killedByTeam` objectives can only track direct kills (so not arrows or explosions, for example).
8. Objectives cannot be displayed as hearts, although literally the only reason is that there's no good half heart character.
9. Team prefixes and suffixes have been replaced with `nameFormat` (for example, `/team modify a_nice_team nameFormat [Admin] %s the great`), where any `%s` is replaced with the player's name. If your name was `singleplayer`, it would appear as `[Admin] singleplayer the great`. The reason for this is pretty simple: I don't want to figure out how to do quotes, and Minetest removes trailing space, meaning prefixes ending in spaces are impossible. This fixes that.
10. The `/give` command is currently unable to give multiple tools (so `/give @s default:pick_wood 5` will only give 1). This may change.
## TODO
- [ ] Add scoreboard playerlist and nametags (?)
- [ ] Figure out feet/eyes since most entities don't have that
- [ ] Make output match ACOVG's (error messages, number results, etc.)
- [ ] Add more scoreboard criteria (settings to disable)
- [ ] `xp`/`level` (MCLA/VL only)
- [ ] `food` (MCLA/VL/stamina)
- [ ] `air`
- [ ] `armor` (MCLA/VL/3D Armor)
- [x] `trigger`
- [ ] `picked_up.<itemstring>`
- [ ] `mined.<itemstring>`
- [ ] `crafted.<itemstring>`
- [ ] `total_world_time`
- [ ] `leave_game`
- [ ] Add missing `execute` subcommands
- [ ] `in`
- [ ] `summon`
- [ ] `if/unless`
- [ ] `biome`
- [ ] `block`/`node`
- [ ] `blocks`/`nodes`
- [ ] `data`
- [ ] `dimension`
- [ ] `entity`
- [ ] `loaded`
- [ ] `score`
- [ ] `store`
- [ ] `block`/`node`
- [ ] `bossbar`
- [ ] `entity`
- [ ] `score`
- [ ] Add more commands
- [x] `trigger`
- [ ] `alwaysday`/`daylock`
- [ ] `ban`/`ban-ip`/`banlist`
- [ ] `bossbar`? (probably significantly modified)
- [ ] `advancement`
- [ ] `fill` (Extra argument for LBM vs `set_node(s)`)
- [ ] `changesetting`?
- [ ] `clear`
- [ ] `spawnpoint`
- [ ] `clearspawnpoint`
- [ ] `clone`
- [ ] `damage`
- [ ] `data`
- [ ] `deop` (removes all but basic privs)
- [ ] `op` (grants certain privs, including `server`)
- [ ] `effect` (MCLA/VL only)
- [ ] `enchant` (MCLA/VL only, also override forceenchant?)
- [ ] `experience`/`xp` (MCLA/VL only)
- [ ] `fog`
- [ ] `forceload`
- [ ] `gamemode` (in MTG, grants/revokes `creative`)
- [ ] `gamerule`? (maybe equivalent to `changesetting`?)
- [ ] `item`?
- [ ] `kick`
- [ ] `list`
- [ ] `locate` (copy from or depend on Wuzzy's `findbiome`, maybe also support MCLA/VL end shrines)
- [ ] `loot`
- [ ] `music` (depending on various mods)
- [ ] `pardon`
- [ ] `pardon-ip`
- [ ] `particle`
- [ ] `place`
- [ ] `random` (although seeds seem to be somewhat inconsistent in MT)
- [ ] `recipe` (MCLA/VL only)
- [ ] `remove`
- [ ] `replaceitem`
- [ ] `return`
- [ ] `ride`?
- [ ] `seed`
- [ ] `setidletimeout`?
- [ ] `spreadplayers`?
- [ ] `stop`
- [ ] `structure`?
- [x] `summon`
- [ ] `tag`
- [ ] `teammsg`/`tm`
- [ ] `testfor`
- [ ] `testforblock`
- [ ] `testforblocks`
- [ ] `tickingarea`?
- [ ] `toggledownfall` (depending on mods)
- [ ] `weather` (depending on mods)
- [ ] `whitelist`
- [ ] `worldborder`? (maybe not visible, probably no collision)
- [ ] Add more selector arguments
- [ ] `dx`/`dy`/`dz`
- [ ] `x_rotation`/`rx`/`rxm`/`y_rotation`/`ry`/`rym`
- [ ] `scores`
- [ ] `tag`
- [ ] `team`
- [ ] `level`/`l`/`lm` (MCLA/VL only)
- [ ] `gamemode`/`l`/`lm` (more of an "is creative?" command)
- [ ] `advancements` (with MCLA/VL/awards), change syntax
- [ ] `haspermission` (privs)

536
entity_aliases.lua Normal file
View File

@ -0,0 +1,536 @@
local S = minetest.get_translator(minetest.get_current_modname())
better_commands.entity_aliases = {
-- Animalia
["animalia:angelfish"] = {angelfish = true},
["animalia:bat"] = {bat = true},
["animalia:bird"] = {bird = true},
["animalia:blue_tang"] = {blue_tang = true},
["animalia:cat"] = {cat = true},
["animalia:chicken"] = {chicken = true},
["animalia:clownfish"] = {clownfish = true},
["animalia:cow"] = {cow = true},
["animalia:fox"] = {fox = true},
["animalia:frog"] = {frog = true},
["animalia:grizzly_bear"] = {grizzly_bear = true},
["animalia:horse"] = {horse = true},
["animalia:opossum"] = {opossom = true, possum = true},
["animalia:owl"] = {owl = true},
["animalia:pig"] = {pig = true},
["animalia:rat"] = {rat = true},
["animalia:reindeer"] = {reindeer = true},
["animalia:sheep"] = {sheep = true},
["animalia:song_bird"] = {song_bird = true, songbird = true},
["animalia:tropical_fish"] = {tropical_fish = true},
["animalia:turkey"] = {turkey = true},
["animalia:wolf"] = {wolf = true},
-- Built-in:
["__builtin:item"] = {item = true},
["__builtin:falling_node"] = {falling_node = true, falling_block = true},
-- Dmobs
["dmobs:badger"] = {badger = true},
["dmobs:butterfly"] = {butterfly = true},
["dmobs:dragon1"] = {dragon = true, dragon1 = true, red_dragon = true, fire_dragon = true},
["dmobs:dragon2"] = {dragon = true, dragon2 = true, black_dragon = true, lightning_dragon = true},
["dmobs:dragon3"] = {dragon = true, dragon3 = true, green_dragon = true, poison_dragon = true},
["dmobs:dragon4"] = {dragon = true, dragon4 = true, blue_dragon = true, ice_dragon = true},
["dmobs:dragon_black"] = {dragon = true, dragon2 = true, black_dragon = true, lightning_dragon = true},
["dmobs:dragon_blue"] = {dragon = true, dragon4 = true, blue_dragon = true, ice_dragon = true},
["dmobs:dragon_great"] = {dragon = true, boss_dragon = true, great_dragon = true},
["dmobs:dragon_green"] = {dragon = true, dragon3 = true, green_dragon = true, poison_dragon = true},
["dmobs:dragon_red"] = {dragon = true, dragon1 = true, red_dragon = true, fire_dragon = true},
["dmobs:elephant"] = {elephant = true},
["dmobs:fox"] = {fox = true},
["dmobs:gnorm"] = {gnorm = true},
["dmobs:golem"] = {golem = true},
["dmobs:golem_friendly"] = {golem = true},
["dmobs:hedgehog"] = {hedgehog = true},
["dmobs:nyan"] = {nyan = true, nyan_cat = true},
["dmobs:ogre"] = {ogre = true},
["dmobs:orc"] = {orc = true},
["dmobs:orc2"] = {orc = true, orc2 = true, morgul_orc = true},
["dmobs:owl"] = {owl = true},
["dmobs:panda"] = {panda = true},
["dmobs:pig"] = {flying_pig = true},
["dmobs:pig_evil"] = {flying_pig = true, kamikaze_pig = true, evil_pig = true},
["dmobs:rat"] = {rat = true},
["dmobs:skeleton"] = {skeleton = true},
["dmobs:tortoise"] = {tortoise = true},
["dmobs:treeman"] = {treeman = true},
["dmobs:wasp"] = {wasp = true},
["dmobs:wasp_leader"] = {wasp = true, wasp_leader = true, king_of_sting = true},
["dmobs:waterdragon"] = {dragon = true, water_dragon = true, boss_water_dragon = true, waterdragon = true, boss_waterdragon = true},
["dmobs:waterdragon_2"] = {dragon = true, water_dragon = true, waterdragon = true},
["dmobs:whale"] = {whale = true},
["dmobs:wyvern"] = {wyvern = true},
-- Draconis
["draconis:fire_dragon"] = {dragon = true, fire_dragon = true},
["draconis:ice_dragon"] = {dragon = true, ice_dragon = true},
["draconis:jungle_wyvern"] = {dragon = true, jungle_wyvern = true},
-- Wilhelmines Living Nether
["livingnether:cyst"] = {cyst = true},
["livingnether:flyingrod"] = {flyingrod = true},
["livingnether:lavawalker"] = {lavawalker = true},
["livingnether:noodlemaster"] = {noodlemaster = true},
["livingnether:razorback"] = {razorback = true},
["livingnether:sokaarcher"] = {soka_archer = true, sokaarcher = true},
["livingnether:sokameele"] = {soka_meele = true, sokameele = true, soka_melee = true, sokamelee = true},
["livingnether:tardigrade"] = {tardigrade = true},
["livingnether:whip"] = {flesh_whip = true, whip = true},
-- Mobs Redo mods:
["mobs_animal:bee"] = {bee = true},
["mobs_animal:cow"] = {cow = true},
["mobs_animal:bunny"] = {bunny = true},
["mobs_animal:chicken"] = {chicken = true},
["mobs_animal:kitten"] = {kitten = true},
["mobs_animal:panda"] = {panda = true},
["mobs_animal:penguin"] = {penguin = true},
["mobs_animal:pumba"] = {pumba = true, pumbaa = true, warthog = true}, -- they misspelled it...
["mobs_animal:rat"] = {rat = true},
["mobs_balrog:balrog"] = {balrog = true},
["mobs_crocs:crocodile"] = {crocodile = true},
["mobs_crocs:crocodile_float"] = {crocodile = true},
["mobs_crocs:crocodile_swim"] = {crocodile = true},
["mobs_fish:clownfish"] = {clownfish = true},
["mobs_fish:tropical"] = {tropical_fish = true},
["mob_horse:horse"] = {horse = true},
["mobs_jellyfish:jellyfish"] = {jellyfish = true},
["mob_mese_monster_classic:mese_monster"] = {mese_monster = true, classic_mese_monster = true},
["mobs_monster:dirt_monster"] = {dirt_monster = true},
["mobs_monster:dungeon_master"] = {dungeon_master = true},
["mobs_monster:fire_spirit"] = {fire_spirit = true},
["mobs_monster:land_guard"] = {land_guard = true},
["mobs_monster:lava_flan"] = {lava_flan = true},
["mobs_monster:mese_monster"] = {mese_monster = true},
["mobs_monster:obsidian_flan"] = {obsidian_flan = true},
["mobs_monster:oerkki"] = {oerkki = true},
["mobs_monster:sand_monster"] = {sand_monster = true},
["mobs_monster:spider"] = {spider = true},
["mobs_monster:stone_monster"] = {stone_monster = true},
["mobs_monster:tree_monster"] = {tree_monster = true},
["mobs_sharks:shark_lg"] = {shark = true},
["mobs_sharks:shark_md"] = {shark = true},
["mobs_sharks:shark_sm"] = {shark = true},
["mobs_skeletons:skeleton"] = {skeleton = true, wither_skeleton = true},
["mobs_skeletons:skeleton_archer"] = {skeleton_archer = true}, -- would have "skeleton" but overlaps
["mobs_skeletons:skeleton_archer_dark"] = {dark_skeleton_archer = true, stray = true},
["mobs_turtles:turtle"] = {turtle = true},
["mobs_turtles:seaturtle"] = {sea_turtle = true, seaturtle = true},
-- MCLA/VL:
["mobs_mc:axolotl"] = {axolotl = true},
["mobs_mc:baby_hoglin"] = {hoglin = true, baby_hoglin = true},
["mobs_mc:baby_husk"] = {husk = true, baby_husk = true},
["mobs_mc:baby_strider"] = {strider = true, baby_strider = true},
["mobs_mc:baby_zombie"] = {zombie = true, baby_zombie = true},
["mobs_mc:bat"] = {bat = true},
["mobs_mc:blaze"] = {blaze = true},
["mobs_mc:cat"] = {cat = true},
["mobs_mc:cave_spider"] = {cave_spider = true},
["mobs_mc:chicken"] = {chicken = true},
["mobs_mc:cod"] = {cod = true},
["mobs_mc:cow"] = {cow = true},
["mobs_mc:creeper"] = {creeper = true},
["mobs_mc:creeper_charged"] = {creeper = true, charged_creeper = true},
["mobs_mc:dog"] = {wolf = true},
["mobs_mc:dolphin"] = {dolphin = true},
["mobs_mc:donkey"] = {donkey = true},
["mobs_mc:enderdragon"] = {ender_dragon = true},
["mobs_mc:enderman"] = {enderman = true},
["mobs_mc:endermite"] = {endermite = true},
["mobs_mc:evoker"] = {evoker = true},
["mobs_mc:ghast"] = {ghast = true},
["mobs_mc:glow_squid"] = {glow_squid = true},
["mobs_mc:guardian"] = {guardian = true},
["mobs_mc:guardian_elder"] = {elder_guardian = true},
["mobs_mc:hoglin"] = {hoglin = true},
["mobs_mc:horse"] = {horse = true},
["mobs_mc:husk"] = {husk = true},
["mobs_mc:illusioner"] = {illusioner = true},
["mobs_mc:iron_golem"] = {iron_golem = true},
["mobs_mc:killer_bunny"] = {rabbit = true, killer_bunny = true},
["mobs_mc:llama"] = {llama = true},
["mobs_mc:magma_cube_big"] = {magma_cube = true, big_magma_cube = true},
["mobs_mc:magma_cube_small"] = {magma_cube = true, small_magma_cube = true},
["mobs_mc:magma_cube_tiny"] = {magma_cube = true, tiny_magma_cube = true},
["mobs_mc:mooshroom"] = {mooshroom = true},
["mobs_mc:mule"] = {mule = true},
["mobs_mc:ocelot"] = {ocelot = true},
["mobs_mc:parrot"] = {parrot = true},
["mobs_mc:pig"] = {pig = true},
["mobs_mc:piglin"] = {piglin = true},
["mobs_mc:piglin_brute"] = {piglin_brute = true},
["mobs_mc:pillager"] = {pillager = true},
["mobs_mc:polar_bear"] = {polar_bear = true},
["mobs_mc:rabbit"] = {rabbit = true},
["mobs_mc:salmon"] = {salmon = true},
["mobs_mc:sheep"] = {sheep = true},
["mobs_mc:shulker"] = {shulker = true},
["mobs_mc:silverfish"] = {silverfish = true},
["mobs_mc:skeleton"] = {skeleton = true},
["mobs_mc:skeleton_horse"] = {skeleton_horse = true},
["mobs_mc:slime_big"] = {slime = true, big_slime = true},
["mobs_mc:slime_small"] = {slime = true, small_slime = true},
["mobs_mc:slime_tiny"] = {slime = true, tiny_slime = true},
["mobs_mc:snowman"] = {snow_golem = true, snowman = true},
["mobs_mc:spider"] = {spider = true},
["mobs_mc:squid"] = {squid = true},
["mobs_mc:stray"] = {stray = true},
["mobs_mc:strider"] = {strider = true},
["mobs_mc:sword_piglin"] = {zombie_piglin = true, zombified_piglin = true},
["mobs_mc:tropical_fish"] = {tropical_fish = true, clownfish = true},
["mobs_mc:vex"] = {vex = true},
["mobs_mc:villager"] = {villager = true},
["mobs_mc:villager_zombie"] = {zombie_villager = true},
["mobs_mc:vindicator"] = {vindicator = true},
["mobs_mc:witch"] = {witch = true},
["mobs_mc:wither"] = {wither = true},
["mobs_mc:witherskeleton"] = {wither_skeleton = true},
["mobs_mc:wolf"] = {wolf = true},
["mobs_mc:zoglin"] = {zoglin = true},
["mobs_mc:zombie"] = {zombie = true},
["mobs_mc:zombie_horse"] = {zombie_horse = true},
["mobs_mc:zombified_piglin"] = {zombie_piglin = true, zombified_piglin = true},
["nssm:ant_queen"] = {ant = true, ant_queen = true},
["nssm:ant_soldier"] = {ant = true, ant_soldier = true},
["nssm:ant_worker"] = {ant = true, ant_worker = true},
["nssm:black_widow"] = {spider = true, black_widow = true, black_widow_spider = true},
["nssm:bloco"] = {bloco = true},
["nssm:crab"] = {crab = true},
["nssm:crocodile"] = {crocodile = true},
["nssm:daddy_long_legs"] = {daddy_long_legs = true},
["nssm:dolidrosaurus"] = {dolidrosaurus = true},
["nssm:duck"] = {duck = true},
["nssm:duckking"] = {duck = true, duck_king = true, duckking = true},
["nssm:echidna"] = {echidna = true},
["nssm:enderduck"] = {duck = true, ender_duck = true, enderduck = true},
["nssm:felucco"] = {felucco = true},
["nssm:flying_duck"] = {duck = true, flying_duck = true},
["nssm:giant_sandworm"] = {sandworm = true, giant_sandworm = true},
["nssm:icelamander"] = {icelamander = true},
["nssm:icesnake"] = {ice_snake = true, icesnake = true},
["nssm:kraken"] = {kraken = true},
["nssm:larva"] = {larva = true},
["nssm:lava_titan"] = {lava_titan = true},
["nssm:manticore"] = {manticore = true},
["nssm:mantis"] = {mantis = true},
["nssm:mantis_beast"] = {mantis = true, mantis_beast = true},
["nssm:masticone"] = {masticone = true},
["nssm:mese_dragon"] = {dragon = true, mese_dragon = true},
["nssm:moonheron"] = {moonheron = true, moon_heron = true},
["nssm:morbat1"] = {morbat = true, morbat1 = true},
["nssm:morbat2"] = {morbat = true, morbat2 = true},
["nssm:morbat3"] = {morbat = true, morbat3 = true},
["nssm:mordain"] = {mordain = true},
["nssm:morde"] = {morde = true},
["nssm:morgre"] = {morgre = true},
["nssm:morgut"] = {morgut = true},
["nssm:morlu"] = {morlu = true},
["nssm:mortick"] = {mortick = true},
["nssm:morvalar"] = {morvalar = true, morvalar7 = true},
["nssm:morvalar6"] = {morvalar = true, morvalar6 = true},
["nssm:morvalar5"] = {morvalar = true, morvalar5 = true},
["nssm:morvalar4"] = {morvalar = true, morvalar4 = true},
["nssm:morvalar3"] = {morvalar = true, morvalar3 = true},
["nssm:morvalar2"] = {morvalar = true, morvalar2 = true},
["nssm:morvalar1"] = {morvalar = true, morvalar1 = true},
["nssm:morvalar0"] = {morvalar = true, morvalar0 = true},
["nssm:morvy"] = {morvy = true},
["nssm:morwa"] = {morwa = true},
["nssm:night_master"] = {night_master = true, night_master_3 = true},
["nssm:night_master_2"] = {night_master = true, night_master_2 = true},
["nssm:night_master_1"] = {night_master = true, night_master_1 = true},
["nssm:octopus"] = {octopus = true},
["nssm:phoenix"] = {phoenix = true},
["nssm:pumpboom_large"] = {pumpboom = true, large_pumpboom = true},
["nssm:pumpboom_medium"] = {pumpboom = true, medium_pumpboom = true},
["nssm:pumpboom_small"] = {pumpboom = true, small_pumpboom = true},
["nssm:pumpking"] = {pumpking = true},
["nssm:sand_bloco"] = {bloco = true, sand_bloco = true},
["nssm:sandworm"] = {sandworm = true},
["nssm:scrausics"] = {scrausics = true},
["nssm:signosigno"] = {signosigno = true},
["nssm:snow_biter"] = {snow_biter = true},
["nssm:spiderduck"] = {duck = true, spiderduck = true, spider_duck = true},
["nssm:stone_eater"] = {stone_eater = true, stoneater = true},
["nssm:swimming_duck"] = {duck = true, swimming_duck = true},
["nssm:tarantula"] = {tarantula = true, spitting_tarantula = true}, -- Had to call them something
["nssm:tarantula_propower"] = {tarantula = true, biting_tarantula = true},
["nssm:uloboros"] = {uloboros = true},
["nssm:werewolf"] = {werewolf = true},
["nssm:white_werewolf"] = {werewolf = true, white_werewolf = true},
["nssm:xgaloctopus"] = {xgaloctopus = true},
}
better_commands.unique_entities = {}
--[[ Basically makes this kind of table:
{
slime = {"mobs_mc:slime_big", "mobs_mc:slime_small", "mobs_mc:slime_tiny"},
wolf = {"mobs_mc:wolf"}
}
]]
minetest.register_on_mods_loaded(function()
local exists
for id, value in pairs(better_commands.entity_aliases) do
if minetest.registered_entities[id] then
for alias in pairs(value) do
if not better_commands.unique_entities[alias] then
better_commands.unique_entities[alias] = {}
end
exists = false
for _, existing_id in ipairs(better_commands.unique_entities[alias]) do
if id == existing_id then
exists = true
end
end
if not exists then
table.insert(better_commands.unique_entities[alias], id)
end
end
end
end
end)
better_commands.entity_names = {
-- Built-in:
-- ["__builtin:item"] = S("Item"), -- Handled separately
-- ["__builtin:falling_node"] = S("Falling Node"), -- Handled separately
-- Animalia
["animalia:angelfish"] = S("Angelfish"),
["animalia:bat"] = S("Bat"),
["animalia:bird"] = S("Bird"),
["animalia:blue_tang"] = S("Blue Tang"),
["animalia:cat"] = S("Cat"),
["animalia:chicken"] = S("Chicken"),
["animalia:clownfish"] = S("Clownfish"),
["animalia:cow"] = S("Cow"),
["animalia:fox"] = S("Fox"),
["animalia:frog"] = S("Frog"),
["animalia:grizzly_bear"] = S("Grizzly Bear"),
["animalia:horse"] = S("Horse"),
["animalia:opossum"] = S("Opossum"),
["animalia:owl"] = S("Owl"),
["animalia:pig"] = S("Pig"),
["animalia:rat"] = S("Rat"),
["animalia:reindeer"] = S("Reindeer"),
["animalia:sheep"] = S("Sheep"),
["animalia:song_bird"] = S("Song Bird"),
["animalia:tropical_fish"] = S("Tropical Fish"),
["animalia:turkey"] = S("Turkey"),
["animalia:wolf"] = S("Wolf"),
["dmobs:badger"] = S("Badger"),
["dmobs:butterfly"] = S("Butterfly"),
["dmobs:dragon1"] = S("Fire Dragon"),
["dmobs:dragon2"] = S("Lightning Dragon"),
["dmobs:dragon3"] = S("Poison Dragon"),
["dmobs:dragon4"] = S("Ice Dragon"),
["dmobs:dragon_black"] = S("Lightning Dragon"),
["dmobs:dragon_blue"] = S("Ice Dragon"),
["dmobs:dragon_great"] = S("Boss Dragon"),
["dmobs:dragon_great_tame"] = S("Boss Dragon"),
["dmobs:dragon_green"] = S("Poison Dragon"),
["dmobs:dragon_red"] = S("Fire Dragon"),
["dmobs:elephant"] = S("Elephant"),
["dmobs:fox"] = S("Fox"),
["dmobs:gnorm"] = S("Gnorm"),
["dmobs:golem"] = S("Golem"),
["dmobs:golem_friendly"] = S("Golem"),
["dmobs:hedgehog"] = S("Hedgehog"),
["dmobs:nyan"] = S("Nyan Cat"),
["dmobs:ogre"] = S("Ogre"),
["dmobs:orc"] = S("Orc"),
["dmobs:orc2"] = S("Morgul Orc"),
["dmobs:owl"] = S("Owl"),
["dmobs:panda"] = S("Panda"),
["dmobs:pig"] = S("Flying Pig"),
["dmobs:pig_evil"] = S("Flying Pig"),
["dmobs:rat"] = S("Rat"),
["dmobs:skeleton"] = S("Skeleton"),
["dmobs:tortoise"] = S("Tortoise"),
["dmobs:treeman"] = S("Treeman"),
["dmobs:wasp"] = S("Wasp"),
["dmobs:wasp_leader"] = S("King of Sting"),
["dmobs:waterdragon"] = S("Boss Waterdragon"),
["dmobs:waterdragon_2"] = S("Waterdragon"),
["dmobs:whale"] = S("Whale"),
["dmobs:wyvern"] = S("Wyvern"),
-- Draconis
["draconis:fire_dragon"] = S("Fire Dragon"),
["draconis:ice_dragon"] = S("Ice Dragon"),
["draconis:jungle_wyvern"] = S("Jungle Wyvern"),
-- Wilhelmines Living Nether
["livingnether:cyst"] = S("Cyst"),
["livingnether:flyingrod"] = S("Flyingrod"),
["livingnether:lavawalker"] = S("Lavawalker"),
["livingnether:noodlemaster"] = S("Noodlemaster"),
["livingnether:razorback"] = S("Razorback"),
["livingnether:sokaarcher"] = S("Soka Archer"),
["livingnether:sokameele"] = S("Soka Melee"),
["livingnether:tardigrade"] = S("Tardigrade"),
["livingnether:whip"] = S("Flesh Whip"),
-- Mobs Redo:
["mobs_animal:bee"] = S("Bee"),
["mobs_animal:cow"] = S("Cow"),
["mobs_animal:bunny"] = S("Bunny"),
["mobs_animal:chicken"] = S("Chicken"),
["mobs_animal:kitten"] = S("Kitten"),
["mobs_animal:panda"] = S("Panda"),
["mobs_animal:penguin"] = S("Penguin"),
["mobs_animal:pumba"] = S("Pumba"),
["mobs_animal:rat"] = S("Rat"),
["mobs_balrog:balrog"] = S("Balrog"),
["mobs_crocs:crocodile"] = S("Crocodile"),
["mobs_crocs:crocodile_float"] = S("Crocodile"),
["mobs_crocs:crocodile_swim"] = S("Crocodile"),
["mobs_fish:clownfish"] = S("Clownfish"),
["mobs_fish:tropical"] = S("Tropical Fish"),
["mob_horse:horse"] = S("Horse"),
["mobs_jellyfish:jellyfish"] = S("Jellyfish"),
["mob_mese_monster_classic:mese_monster"] = S("Mese Monster"),
["mobs_monster:dirt_monster"] = S("Dirt Monster"),
["mobs_monster:dungeon_master"] = S("Dungeon Master"),
["mobs_monster:fire_spirit"] = S("Fire Spirit"),
["mobs_monster:land_guard"] = S("Land Guard"),
["mobs_monster:lava_flan"] = S("Lava Flan"),
["mobs_monster:mese_monster"] = S("Mese Monster"),
["mobs_monster:obsidian_flan"] = S("Obsidian Flan"),
["mobs_monster:oerkki"] = S("Oerkki"),
["mobs_monster:sand_monster"] = S("Sand Monster"),
["mobs_monster:spider"] = S("Spider"),
["mobs_monster:stone_monster"] = S("Stone Monster"),
["mobs_monster:tree_monster"] = S("Tree Monster"),
["mobs_sharks:shark_lg"] = S("Shark"),
["mobs_sharks:shark_md"] = S("Shark"),
["mobs_sharks:shark_sm"] = S("Shark"),
["mobs_skeletons:skeleton"] = S("Skeleton"),
["mobs_skeletons:skeleton_archer"] = S("Skeleton Archer"),
["mobs_skeletons:skeleton_archer_dark"] = S("Dark Skeleton Archer"),
["mobs_turtles:turtle"] = S("Turtle"),
["mobs_turtles:seaturtle"] = S("Sea Turtle"),
-- MCLA/VL mobs all use luaentity.description
["nssm:ant_queen"] = S("Ant Queen"),
["nssm:ant_soldier"] = S("Ant Soldier"),
["nssm:ant_worker"] = S("Ant Worker"),
["nssm:black_widow"] = S("Black Widow"),
["nssm:bloco"] = S("Bloco"),
["nssm:crab"] = S("Crab"),
["nssm:crocodile"] = S("Crocodile"),
["nssm:daddy_long_legs"] = S("Daddy Long Legs"),
["nssm:dolidrosaurus"] = S("Dolidrosaurus"),
["nssm:duck"] = S("Duck"),
["nssm:duckking"] = S("Duckking"),
["nssm:echidna"] = S("Echidna"),
["nssm:enderduck"] = S("Enderduck"),
["nssm:felucco"] = S("Felucco"),
["nssm:flying_duck"] = S("Flying Duck"),
["nssm:giant_sandworm"] = S("Giant Sandworm"),
["nssm:icelamander"] = S("Icelamander"),
["nssm:icesnake"] = S("Icesnake"),
["nssm:kraken"] = S("Kraken"),
["nssm:larva"] = S("Larva"),
["nssm:lava_titan"] = S("Lava Titan"),
["nssm:manticore"] = S("Manticore"),
["nssm:mantis"] = S("Mantis"),
["nssm:mantis_beast"] = S("Mantis Beast"),
["nssm:masticone"] = S("Masticone"),
["nssm:mese_dragon"] = S("Mese Dragon"),
["nssm:moonheron"] = S("Moonheron"),
["nssm:morbat1"] = S("Morbat"),
["nssm:morbat2"] = S("Morbat"),
["nssm:morbat3"] = S("Morbat"),
["nssm:mordain"] = S("Mordain"),
["nssm:morde"] = S("Morde"),
["nssm:morgre"] = S("Morgre"),
["nssm:morgut"] = S("Morgut"),
["nssm:morlu"] = S("Morlu"),
["nssm:mortick"] = S("Mortick"),
["nssm:morvalar"] = S("Morvalar"),
["nssm:morvalar6"] = S("Morvalar"),
["nssm:morvalar5"] = S("Morvalar"),
["nssm:morvalar4"] = S("Morvalar"),
["nssm:morvalar3"] = S("Morvalar"),
["nssm:morvalar2"] = S("Morvalar"),
["nssm:morvalar1"] = S("Morvalar"),
["nssm:morvalar0"] = S("Morvalar"),
["nssm:morvy"] = S("Morvy"),
["nssm:morwa"] = S("Morwa"),
["nssm:night_master"] = S("Night Master"),
["nssm:night_master_2"] = S("Night Master"),
["nssm:night_master_1"] = S("Night Master"),
["nssm:octopus"] = S("Octopus"),
["nssm:phoenix"] = S("Phoenix"),
["nssm:pumpboom_large"] = S("Pumpboom"),
["nssm:pumpboom_medium"] = S("Pumpboom"),
["nssm:pumpboom_small"] = S("Pumpboom"),
["nssm:pumpking"] = S("Pumpking"),
["nssm:sand_bloco"] = S("Sand Bloco"),
["nssm:sandworm"] = S("Sandworm"),
["nssm:scrausics"] = S("Scrausics"),
["nssm:signosigno"] = S("Signosigno"),
["nssm:snow_biter"] = S("Snow Biter"),
["nssm:spiderduck"] = S("Spiderduck"),
["nssm:stone_eater"] = S("Stoneater"),
["nssm:swimming_duck"] = S("Swimming Duck"),
["nssm:tarantula"] = S("Tarantula"),
["nssm:tarantula_propower"] = S("Tarantula"),
["nssm:uloboros"] = S("Uloboros"),
["nssm:werewolf"] = S("Werewolf"),
["nssm:white_werewolf"] = S("Werewolf"),
["nssm:xgaloctopus"] = S("Xgaloctobus"),
}
local sheep_colors = {
white = "White",
black = "Black",
blue = "Blue",
cyan = "Cyan",
grey = "Grey",
magenta = "Magenta",
orange = "Orange",
pink = "Pink",
red = "Red",
violet = "Violet",
yellow = "Yellow",
dark_green = "Dark Green",
green = "Green",
dark_grey = "Dark Grey",
brown = "Brown"
}
for color, title in pairs(sheep_colors) do
better_commands.entity_aliases["mobs_animal:sheep_"..color] = {[color.."_sheep"] = true, sheep = true}
better_commands.entity_names["mobs_animal:sheep_"..color] = S("@1 Sheep", S(title))
end

23
init.lua Normal file
View File

@ -0,0 +1,23 @@
better_commands = {commands = {}, old_commands = {}, players = {}}
better_commands.override = minetest.settings:get_bool("better_commands_override", false)
better_commands.mc_time = minetest.settings:get_bool("better_commands_mc_time", false)
better_commands.save_interval = tonumber(minetest.settings:get("better_commands_save_interval")) or 3
better_commands.kill_creative_players = minetest.settings:get_bool("better_commands_kill_creative_players", false)
better_commands.mcl = minetest.get_modpath("mcl_core")
local modpath = minetest.get_modpath("better_commands")
dofile(modpath.."/entity_aliases.lua")
dofile(modpath.."/API/api.lua")
dofile(modpath.."/COMMANDS/commands.lua")
-- Build list of all registered players (https://forum.minetest.net/viewtopic.php?t=21582)
minetest.after(0,function()
for name in minetest.get_auth_handler().iterate() do
better_commands.players[name] = true
end
end)
minetest.register_on_newplayer(function(player)
better_commands.players[player:get_player_name()] = true
end)

4
mod.conf Normal file
View File

@ -0,0 +1,4 @@
name = better_commands
title = Better Commands
description = Adds commands and syntax from a certain other voxel game
supported_games = *

504
mod_translation_updater.py Normal file
View File

@ -0,0 +1,504 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#
# Script to generate Minetest translation template files and update
# translation files.
#
# Copyright (C) 2019 Joachim Stolberg, 2020 FaceDeer, 2020 Louis Royer,
# 2023 Wuzzy.
# License: LGPLv2.1 or later (see LICENSE file for details)
import os, fnmatch, re, shutil, errno
from sys import argv as _argv
from sys import stderr as _stderr
# Running params
params = {"recursive": False,
"help": False,
"verbose": False,
"folders": [],
"old-file": False,
"break-long-lines": False,
"print-source": False,
"truncate-unused": False,
}
# Available CLI options
options = {"recursive": ['--recursive', '-r'],
"help": ['--help', '-h'],
"verbose": ['--verbose', '-v'],
"old-file": ['--old-file', '-o'],
"break-long-lines": ['--break-long-lines', '-b'],
"print-source": ['--print-source', '-p'],
"truncate-unused": ['--truncate-unused', '-t'],
}
# Strings longer than this will have extra space added between
# them in the translation files to make it easier to distinguish their
# beginnings and endings at a glance
doublespace_threshold = 80
# These symbols mark comment lines showing the source file name.
# A comment may look like "##[ init.lua ]##".
symbol_source_prefix = "##["
symbol_source_suffix = "]##"
# comment to mark the section of old/unused strings
comment_unused = "##### not used anymore #####"
def set_params_folders(tab: list):
'''Initialize params["folders"] from CLI arguments.'''
# Discarding argument 0 (tool name)
for param in tab[1:]:
stop_param = False
for option in options:
if param in options[option]:
stop_param = True
break
if not stop_param:
params["folders"].append(os.path.abspath(param))
def set_params(tab: list):
'''Initialize params from CLI arguments.'''
for option in options:
for option_name in options[option]:
if option_name in tab:
params[option] = True
break
def print_help(name):
'''Prints some help message.'''
print(f'''SYNOPSIS
{name} [OPTIONS] [PATHS...]
DESCRIPTION
{', '.join(options["help"])}
prints this help message
{', '.join(options["recursive"])}
run on all subfolders of paths given
{', '.join(options["old-file"])}
create *.old files
{', '.join(options["break-long-lines"])}
add extra line breaks before and after long strings
{', '.join(options["print-source"])}
add comments denoting the source file
{', '.join(options["verbose"])}
add output information
{', '.join(options["truncate-unused"])}
delete unused strings from files
''')
def main():
'''Main function'''
set_params(_argv)
set_params_folders(_argv)
if params["help"]:
print_help(_argv[0])
else:
# Add recursivity message
print("Running ", end='')
if params["recursive"]:
print("recursively ", end='')
# Running
if len(params["folders"]) >= 2:
print("on folder list:", params["folders"])
for f in params["folders"]:
if params["recursive"]:
run_all_subfolders(f)
else:
update_folder(f)
elif len(params["folders"]) == 1:
print("on folder", params["folders"][0])
if params["recursive"]:
run_all_subfolders(params["folders"][0])
else:
update_folder(params["folders"][0])
else:
print("on folder", os.path.abspath("./"))
if params["recursive"]:
run_all_subfolders(os.path.abspath("./"))
else:
update_folder(os.path.abspath("./"))
# Group 2 will be the string, groups 1 and 3 will be the delimiters (" or ')
# See https://stackoverflow.com/questions/46967465/regex-match-text-in-either-single-or-double-quote
pattern_lua_quoted = re.compile(
r'(?:^|[\.=,{\(\s])' # Look for beginning of file or anything that isn't a function identifier
r'N?F?S\s*\(\s*' # Matches S, FS, NS or NFS function call
r'(["\'])((?:\\\1|(?:(?!\1)).)*)(\1)' # Quoted string
r'[\s,\)]', # End of call or argument
re.DOTALL)
# Handles the [[ ... ]] string delimiters
pattern_lua_bracketed = re.compile(
r'(?:^|[\.=,{\(\s])' # Same as for pattern_lua_quoted
r'N?F?S\s*\(\s*' # Same as for pattern_lua_quoted
r'\[\[(.*?)\]\]' # [[ ... ]] string delimiters
r'[\s,\)]', # Same as for pattern_lua_quoted
re.DOTALL)
# Handles "concatenation" .. " of strings"
pattern_concat = re.compile(r'["\'][\s]*\.\.[\s]*["\']', re.DOTALL)
# Handles a translation line in *.tr file.
# Group 1 is the source string left of the equals sign.
# Group 2 is the translated string, right of the equals sign.
pattern_tr = re.compile(
r'(.*)' # Source string
# the separating equals sign, if NOT preceded by @, unless
# that @ is preceded by another @
r'(?:(?<!(?<!@)@)=)'
r'(.*)' # Translation string
)
pattern_name = re.compile(r'^name[ ]*=[ ]*([^ \n]*)')
pattern_tr_filename = re.compile(r'\.tr$')
# Matches bad use of @ signs in Lua string
pattern_bad_luastring = re.compile(
r'^@$|' # single @, OR
r'[^@]@$|' # trailing unescaped @, OR
r'(?<!@)@(?=[^@1-9n])' # an @ that is not escaped or part of a placeholder
)
# Attempt to read the mod's name from the mod.conf file or folder name. Returns None on failure
def get_modname(folder):
try:
with open(os.path.join(folder, "mod.conf"), "r", encoding='utf-8') as mod_conf:
for line in mod_conf:
match = pattern_name.match(line)
if match:
return match.group(1)
except FileNotFoundError:
folder_name = os.path.basename(folder)
# Special case when run in Minetest's builtin directory
return "__builtin" if folder_name == "builtin" else folder_name
# If there are already .tr files in /locale, returns a list of their names
def get_existing_tr_files(folder):
out = []
for root, dirs, files in os.walk(os.path.join(folder, 'locale/')):
for name in files:
if pattern_tr_filename.search(name):
out.append(name)
return out
# from https://stackoverflow.com/questions/600268/mkdir-p-functionality-in-python/600612#600612
# Creates a directory if it doesn't exist, silently does
# nothing if it already exists
def mkdir_p(path):
try:
os.makedirs(path)
except OSError as exc: # Python >2.5
if exc.errno == errno.EEXIST and os.path.isdir(path):
pass
else: raise
# Converts the template dictionary to a text to be written as a file
# dKeyStrings is a dictionary of localized string to source file sets
# dOld is a dictionary of existing translations and comments from
# the previous version of this text
def strings_to_text(dkeyStrings, dOld, mod_name, header_comments, textdomain, templ = None):
# if textdomain is specified, insert it at the top
if textdomain != None:
lOut = [textdomain] # argument is full textdomain line
# otherwise, use mod name as textdomain automatically
else:
lOut = [f"# textdomain: {mod_name}"]
if templ is not None and templ[2] and (header_comments is None or not header_comments.startswith(templ[2])):
# header comments in the template file
lOut.append(templ[2])
if header_comments is not None:
lOut.append(header_comments)
dGroupedBySource = {}
for key in dkeyStrings:
sourceList = list(dkeyStrings[key])
sourceString = "\n".join(sourceList)
listForSource = dGroupedBySource.get(sourceString, [])
listForSource.append(key)
dGroupedBySource[sourceString] = listForSource
lSourceKeys = list(dGroupedBySource.keys())
lSourceKeys.sort()
for source in lSourceKeys:
localizedStrings = dGroupedBySource[source]
if params["print-source"]:
if lOut[-1] != "":
lOut.append("")
lOut.append(source)
for localizedString in localizedStrings:
val = dOld.get(localizedString, {})
translation = val.get("translation", "")
comment = val.get("comment")
templ_comment = None
if templ:
templ_val = templ[0].get(localizedString, {})
templ_comment = templ_val.get("comment")
if params["break-long-lines"] and len(localizedString) > doublespace_threshold and not lOut[-1] == "":
lOut.append("")
if templ_comment != None and templ_comment != "" and (comment is None or comment == "" or not comment.startswith(templ_comment)):
lOut.append(templ_comment)
if comment != None and comment != "" and not comment.startswith("# textdomain:"):
lOut.append(comment)
lOut.append(f"{localizedString}={translation}")
if params["break-long-lines"] and len(localizedString) > doublespace_threshold:
lOut.append("")
unusedExist = False
if not params["truncate-unused"]:
for key in dOld:
if key not in dkeyStrings:
val = dOld[key]
translation = val.get("translation")
comment = val.get("comment")
# only keep an unused translation if there was translated
# text or a comment associated with it
if translation != None and (translation != "" or comment):
if not unusedExist:
unusedExist = True
lOut.append("\n\n" + comment_unused + "\n")
if params["break-long-lines"] and len(key) > doublespace_threshold and not lOut[-1] == "":
lOut.append("")
if comment != None:
lOut.append(comment)
lOut.append(f"{key}={translation}")
if params["break-long-lines"] and len(key) > doublespace_threshold:
lOut.append("")
return "\n".join(lOut) + '\n'
# Writes a template.txt file
# dkeyStrings is the dictionary returned by generate_template
def write_template(templ_file, dkeyStrings, mod_name):
# read existing template file to preserve comments
existing_template = import_tr_file(templ_file)
text = strings_to_text(dkeyStrings, existing_template[0], mod_name, existing_template[2], existing_template[3])
mkdir_p(os.path.dirname(templ_file))
with open(templ_file, "wt", encoding='utf-8') as template_file:
template_file.write(text)
# Gets all translatable strings from a lua file
def read_lua_file_strings(lua_file):
lOut = []
with open(lua_file, encoding='utf-8') as text_file:
text = text_file.read()
text = re.sub(pattern_concat, "", text)
strings = []
for s in pattern_lua_quoted.findall(text):
strings.append(s[1])
for s in pattern_lua_bracketed.findall(text):
strings.append(s)
for s in strings:
found_bad = pattern_bad_luastring.search(s)
if found_bad:
print("SYNTAX ERROR: Unescaped '@' in Lua string: " + s)
continue
s = s.replace('\\"', '"')
s = s.replace("\\'", "'")
s = s.replace("\n", "@n")
s = s.replace("\\n", "@n")
s = s.replace("=", "@=")
lOut.append(s)
return lOut
# Gets strings from an existing translation file
# returns both a dictionary of translations
# and the full original source text so that the new text
# can be compared to it for changes.
# Returns also header comments in the third return value.
def import_tr_file(tr_file):
dOut = {}
text = None
in_header = True
header_comments = None
textdomain = None
if os.path.exists(tr_file):
with open(tr_file, "r", encoding='utf-8') as existing_file :
# save the full text to allow for comparison
# of the old version with the new output
text = existing_file.read()
existing_file.seek(0)
# a running record of the current comment block
# we're inside, to allow preceeding multi-line comments
# to be retained for a translation line
latest_comment_block = None
for line in existing_file.readlines():
line = line.rstrip('\n')
# "##### not used anymore #####" comment
if line == comment_unused:
# Always delete the 'not used anymore' comment.
# It will be re-added to the file if neccessary.
latest_comment_block = None
if header_comments != None:
in_header = False
continue
# Comment lines
elif line.startswith("#"):
# Source file comments: ##[ file.lua ]##
if line.startswith(symbol_source_prefix) and line.endswith(symbol_source_suffix):
# This line marks the end of header comments.
if params["print-source"]:
in_header = False
# Remove those comments; they may be added back automatically.
continue
# Store first occurance of textdomain
# discard all subsequent textdomain lines
if line.startswith("# textdomain:"):
if textdomain == None:
textdomain = line
continue
elif in_header:
# Save header comments (normal comments at top of file)
if not header_comments:
header_comments = line
else:
header_comments = header_comments + "\n" + line
else:
# Save normal comments
if line.startswith("# textdomain:") and textdomain == None:
textdomain = line
elif not latest_comment_block:
latest_comment_block = line
else:
latest_comment_block = latest_comment_block + "\n" + line
continue
match = pattern_tr.match(line)
if match:
# this line is a translated line
outval = {}
outval["translation"] = match.group(2)
if latest_comment_block:
# if there was a comment, record that.
outval["comment"] = latest_comment_block
latest_comment_block = None
in_header = False
dOut[match.group(1)] = outval
return (dOut, text, header_comments, textdomain)
# like os.walk but returns sorted filenames
def sorted_os_walk(folder):
tuples = []
t = 0
for root, dirs, files in os.walk(folder):
tuples.append( (root, dirs, files) )
t = t + 1
tuples = sorted(tuples)
paths_and_files = []
f = 0
for tu in tuples:
root = tu[0]
dirs = tu[1]
files = tu[2]
files = sorted(files, key=str.lower)
for filename in files:
paths_and_files.append( (os.path.join(root, filename), filename) )
f = f + 1
return paths_and_files
# Walks all lua files in the mod folder, collects translatable strings,
# and writes it to a template.txt file
# Returns a dictionary of localized strings to source file lists
# that can be used with the strings_to_text function.
def generate_template(folder, mod_name):
dOut = {}
paths_and_files = sorted_os_walk(folder)
for paf in paths_and_files:
fullpath_filename = paf[0]
filename = paf[1]
if fnmatch.fnmatch(filename, "*.lua"):
found = read_lua_file_strings(fullpath_filename)
if params["verbose"]:
print(f"{fullpath_filename}: {str(len(found))} translatable strings")
for s in found:
sources = dOut.get(s, set())
sources.add(os.path.relpath(fullpath_filename, start=folder))
dOut[s] = sources
if len(dOut) == 0:
return None
# Convert source file set to list, sort it and add comment symbols.
# Needed because a set is unsorted and might result in unpredictable.
# output orders if any source string appears in multiple files.
for d in dOut:
sources = dOut.get(d, set())
sources = sorted(list(sources), key=str.lower)
newSources = []
for i in sources:
i = i.replace("\\", "/")
newSources.append(f"{symbol_source_prefix} {i} {symbol_source_suffix}")
dOut[d] = newSources
templ_file = os.path.join(folder, "locale/template.txt")
write_template(templ_file, dOut, mod_name)
new_template = import_tr_file(templ_file) # re-import to get all new data
return (dOut, new_template)
# Updates an existing .tr file, copying the old one to a ".old" file
# if any changes have happened
# dNew is the data used to generate the template, it has all the
# currently-existing localized strings
def update_tr_file(dNew, templ, mod_name, tr_file):
if params["verbose"]:
print(f"updating {tr_file}")
tr_import = import_tr_file(tr_file)
dOld = tr_import[0]
textOld = tr_import[1]
textNew = strings_to_text(dNew, dOld, mod_name, tr_import[2], tr_import[3], templ)
if textOld and textOld != textNew:
print(f"{tr_file} has changed.")
if params["old-file"]:
shutil.copyfile(tr_file, f"{tr_file}.old")
with open(tr_file, "w", encoding='utf-8') as new_tr_file:
new_tr_file.write(textNew)
# Updates translation files for the mod in the given folder
def update_mod(folder):
if not os.path.exists(os.path.join(folder, "init.lua")):
print(f"Mod folder {folder} is missing init.lua, aborting.")
exit(1)
assert not is_modpack(folder)
modname = get_modname(folder)
print(f"Updating translations for {modname}")
(data, templ) = generate_template(folder, modname)
if data == None:
print(f"No translatable strings found in {modname}")
else:
for tr_file in get_existing_tr_files(folder):
update_tr_file(data, templ, modname, os.path.join(folder, "locale/", tr_file))
def is_modpack(folder):
return os.path.exists(os.path.join(folder, "modpack.txt")) or os.path.exists(os.path.join(folder, "modpack.conf"))
def is_game(folder):
return os.path.exists(os.path.join(folder, "game.conf")) and os.path.exists(os.path.join(folder, "mods"))
# Determines if the folder being pointed to is a game, mod or a mod pack
# and then runs update_mod accordingly
def update_folder(folder):
if is_game(folder):
run_all_subfolders(os.path.join(folder, "mods"))
elif is_modpack(folder):
run_all_subfolders(folder)
else:
update_mod(folder)
print("Done.")
def run_all_subfolders(folder):
for modfolder in [f.path for f in os.scandir(folder) if f.is_dir() and not f.name.startswith('.')]:
update_folder(modfolder)
main()

11
settingtypes.txt Normal file
View File

@ -0,0 +1,11 @@
# Override commands if they already exist (such as /kill)
better_commands_override (Override existing commands?) bool false
# Use ACOVG times for the /time command? If enabled, day starts at 0, not 7000.
better_commands_mc_time (Use ACOVG time?) bool false
# Can the /kill command kill players in creative mode?
better_commands_kill_creative_players (Kill creative players?) bool false
# Frequency of saving scoreboard/team data and updating sidebar (seconds)
better_commands_save_interval (Save interval) float 3

Binary file not shown.

After

Width:  |  Height:  |  Size: 582 B