Removed all references to ACOVG (recreated repo)
This commit is contained in:
commit
24dee29e84
51
.luacheckrc
Normal file
51
.luacheckrc
Normal 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
33
.vscode/settings.json
vendored
Normal 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
61
API/API.lua
Normal 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
123
API/damage.lua
Normal 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
161
API/entity.lua
Normal 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
617
API/parsing.lua
Normal 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
36
API/register.lua
Normal 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
226
API/scoreboard.lua
Normal 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
64
API/teams.lua
Normal 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
4
CHANGELOG.md
Normal 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
30
COMMANDS/COMMANDS.lua
Normal 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
79
COMMANDS/ability.lua
Normal 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
88
COMMANDS/chat.lua
Normal 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
|
||||||
|
})
|
52
COMMANDS/command_runners.lua
Normal file
52
COMMANDS/command_runners.lua
Normal 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
332
COMMANDS/execute.lua
Normal 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
85
COMMANDS/give.lua
Normal 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
56
COMMANDS/kill.lua
Normal 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
65
COMMANDS/playsound.lua
Normal 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
607
COMMANDS/scoreboard.lua
Normal 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
43
COMMANDS/setblock.lua
Normal 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
34
COMMANDS/summon.lua
Normal 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
196
COMMANDS/team.lua
Normal 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
129
COMMANDS/teleport.lua
Normal 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
51
COMMANDS/time.lua
Normal 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
21
LICENSE.txt
Normal 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
207
README.md
Normal 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
536
entity_aliases.lua
Normal 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
23
init.lua
Normal 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
4
mod.conf
Normal 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
504
mod_translation_updater.py
Normal 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
11
settingtypes.txt
Normal 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
|
BIN
textures/better_commands_scoreboard_bg.png
Normal file
BIN
textures/better_commands_scoreboard_bg.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 582 B |
Loading…
x
Reference in New Issue
Block a user