2024-05-29 08:10:11 -07:00

634 lines
39 KiB
Lua

--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 = S("objectives|players ..."),
description = S("Manupulates the scoreboard"),
privs = {server = true},
func = function(name, param, context)
local split_param = better_commands.parse_params(param)
if not (split_param[1] and split_param[2]) then
return false, better_commands.error(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, better_commands.error(S("Missing name")), 0 end
if better_commands.scoreboard.objectives[objective_name] then
return false, better_commands.error(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, better_commands.error(S("Missing criterion")), 0 end
if not better_commands.validate_criterion(criterion) then
return false, better_commands.error(S("Invalid criterion @1", criterion)), 0
end
local display_name = (split_param[5] and param:sub(split_param[5][1], -1)) 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, better_commands.error(S("Missing objective")), 0 end
if not better_commands.scoreboard.objectives[objective] then
return false, better_commands.error(S("Unknown scoreboard objective '@1'", objective)), 0
end
local key = split_param[4] and split_param[4][3]
if not key then return false, better_commands.error(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, better_commands.error(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, better_commands.error(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, better_commands.error(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, better_commands.error(S("Must be 'blank', 'fixed', or 'styled'")), 0
end
else
return false, better_commands.error(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, better_commands.error(S("Missing objective")), 0 end
if not better_commands.scoreboard.objectives[objective] then
return false, better_commands.error(S("Unknown scoreboard objective '@1'", objective)), 0
end
better_commands.scoreboard.objectives[objective] = nil
for display, data in pairs(better_commands.scoreboard.objectives.displays) do
if data.objective == objective then
better_commands.scoreboard.objectives.displays[display] = nil
end
end
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, better_commands.error(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, better_commands.error(S("Unknown scoreboard objective '@1'", objective)), 0
end
local display, sortable
if location == "list" then
return false, better_commands.error(S("`list` support has not been added yet.")), 0
elseif location == "below_name" then
better_commands.scoreboard.displays.below_name = {objective = objective}
display = better_commands.scoreboard.displays.below_name
sortable = false
elseif location == "sidebar" then
better_commands.scoreboard.displays.sidebar = {objective = objective}
display = better_commands.scoreboard.displays.sidebar
sortable = true
else
local color = location:match("^sidebar%.team.(.+)")
if not color then
return false, better_commands.error(S("Must be 'list', 'below_name', 'sidebar', or 'sidebar.team.<color>")), 0
elseif better_commands.team_colors[color] then
display = better_commands.scoreboard.displays.colors[color]
better_commands.scoreboard.displays.colors[color] = {objective = objective}
else
return false, better_commands.error(S("Invalid color: @1", color)), 0
end
end
local sort = split_param[5] and split_param[5][3]
if sort then
if sortable then
if sort == "ascending" then
display.ascending = true
elseif sort ~= "descending" then
return false, better_commands.error(S("Expected ascending|descending, got @1", sort)), 0
end
else
return false, better_commands.error(S("Display slot @1 does not support sorting.", location)), 0
end
end
return true, S("Set display slot @1 to show objective @2", location, objective), 1
else
return false, better_commands.error(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, better_commands.error(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, better_commands.error(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, better_commands.error(S("Missing score")), 0 end
score = math.floor(score)
local names, err = better_commands.get_scoreboard_names(selector, context, objective)
if err or not names then return false, better_commands.error(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, better_commands.error(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, better_commands.error(S("Must be 'name' or 'numberformat'")), 0 end
if key == "name" then
local selector = split_param[4]
if not selector then return false, better_commands.error(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, better_commands.error(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, better_commands.error(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, better_commands.error(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, better_commands.error(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, better_commands.error(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, better_commands.error(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, better_commands.error(S("Invalid color")), 0
end
end
result = {type = "color", data = format}
return_value = S("@1 set to @2", "numberformat", format)
else
return false, better_commands.error(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, better_commands.error(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, better_commands.error(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, better_commands.error(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, better_commands.error(S("Invalid objective: @1", objective)), 0
end
if objective_data.criterion ~= "trigger" then
return false, better_commands.error(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, better_commands.error(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, better_commands.error(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, better_commands.error(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, better_commands.error(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, better_commands.error(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, better_commands.error(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, better_commands.error(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, better_commands.error(S("Missing source selector")), 0 end
local source_objective = split_param[4] and split_param[4][3]
if not source_objective then return false, better_commands.error(S("Missing source objective")), 0 end
if not better_commands.scoreboard.objectives[source_objective] then
return false, better_commands.error(S("Invalid source objective")), 0
end
local operator = split_param[5] and split_param[5][3]
if not operator then return false, better_commands.error(S("Missing operator")), 0 end
if not scoreboard_operators[operator] then
return false, better_commands.error(S("Invalid operator: @1", operator)), 0
end
local target_selector = split_param[6]
if not target_selector then return false, better_commands.error(S("Missing target selector")), 0 end
local target_objective = split_param[7] and split_param[7][3]
if not target_objective then return false, better_commands.error(S("Missing target objective")), 0 end
if not better_commands.scoreboard.objectives[target_objective] then
return false, better_commands.error(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, better_commands.error(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, better_commands.error(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 = math.floor(target_scores[target].score + source_scores[source].score)
op_string, preposition = "Added", "to"
elseif operator == "-=" then
target_scores[target].score = math.floor(target_scores[target].score - source_scores[source].score)
op_string, preposition = "Subtracted", "from"
elseif operator == "*=" then
target_scores[target].score = math.floor(target_scores[target].score * source_scores[source].score)
op_string, preposition, swap = "Multiplied", "by", true
elseif operator == "/=" then
if source_scores[source].score == 0 then
minetest.chat_send_player(name, S("Skipping attempt to divide by zero"))
else
target_scores[target].score = math.floor(target_scores[target].score / source_scores[source].score)
op_string, preposition, swap = "Divided", "by", true
end
elseif operator == "%=" then
if source_scores[source].score == 0 then
minetest.chat_send_player(name, S("Skipping attempt to divide by zero"))
else
target_scores[target].score = math.floor(target_scores[target].score % source_scores[source].score)
op_string, preposition, swap = "Modulo-ed (?)", "and", true
end
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, better_commands.error(S("No entity was 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, better_commands.error(S("Missing selector")), 0 end
local objective = split_param[4] and split_param[4][3]
if not objective then return false, better_commands.error(S("Missing objective")), 0 end
if not better_commands.scoreboard.objectives[objective] then
return false, better_commands.error(S("Invalid objective")), 0
end
local min = split_param[5] and split_param[5][3]
if not min then return false, better_commands.error(S("Missing min")), 0 end
---@diagnostic disable-next-line: cast-local-type
min = tonumber(min)
if not min then return false, better_commands.error(S("Must be a number")), 0 end
local max = split_param[6] and split_param[6][3]
if not max then return false, better_commands.error(S("Missing max")), 0 end
max = tonumber(max)
if not max then return false, better_commands.error(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, better_commands.error(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, better_commands.error(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, better_commands.error(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, better_commands.error(S("Invalid objective")), 0
end
local names, err = better_commands.get_scoreboard_names(selector, context)
if err or not names then return false, better_commands.error(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, better_commands.error(S("Missing selector")), 0 end
local objective = split_param[4] and split_param[4][3]
if not objective then return false, better_commands.error(S("Missing objective")), 0 end
if not better_commands.scoreboard.objectives[objective] then
return false, better_commands.error(S("Invalid objective")), 0
end
local min = split_param[5] and split_param[5][3]
if not min then return false, better_commands.error(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, better_commands.error(S("Must be a number")), 0 end
local max = split_param[6] and split_param[6][3]
if not max then return false, better_commands.error(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, better_commands.error(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, better_commands.error(err), 0 end
local scoreboard_name = names[1]
local scores = better_commands.scoreboard.objectives[objective].scores
if not scores[scoreboard_name] then
return false, better_commands.error(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, better_commands.error(S("Score @1 is NOT in range @2 to @3", scores[scoreboard_name].score, min, max)), 0
end
else
return false, better_commands.error(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)
if not (context.executor.is_player and context.executor:is_player()) then
return false, better_commands.error(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, better_commands.error(S("Unknown scoreboard objective '@1'", objective)), 0
end
if objective_data.criterion ~= "trigger" then
return false, better_commands.error(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, better_commands.error(S("You cannot trigger this objective yet")), 0
end
if not scores.enabled then
return false, better_commands.error(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, better_commands.error(S("Missing value")), 0 end
value = tonumber(value)
if not value then return false, better_commands.error(S("Value must be a number")), 0 end
if subcommand == "add" then
scores.score = scores.score + math.floor(value)
scores.enabled = false
return true, S("Triggered [@1] (added @2 to value)", display_name, value), scores.score
elseif subcommand == "set" then
scores.score = math.floor(value)
scores.enabled = false
return true, S("Triggered [@1] (set value to @2)", display_name, value), scores.score
else
return false, better_commands.error(S("Expected 'add' or 'set', got @1", subcommand)), 0
end
end
end
})