--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.")), 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 = " [add|set ]", 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 })