minestats/API.lua

1070 lines
44 KiB
Lua

-- Minestats mod for Minetest
-- Copyright © 2017-2019 Alex Yst <mailto:copyright@y.st>
-- This program is free software; you can redistribute it and/or
-- modify it under the terms of the GNU Lesser General Public
-- License as published by the Free Software Foundation; either
-- version 2.1 of the License, or (at your option) any later version.
-- This software is distributed in the hope that it will be useful,
-- but WITHOUT ANY WARRANTY; without even the implied warranty of
-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
-- Lesser General Public License for more details.
-- You should have received a copy of the GNU Lesser General Public
-- License along with this program. If not, see
-- <https://www.gnu.org./licenses/>.
-----------------------------------------------------------------------
------------------------- Internal Variables: -------------------------
-----------------------------------------------------------------------
-- Anything in this table is a countable node/drop pair. Anything else
-- is, well, not.
--
-- Third-party mod developers need not worry about the structure of
-- this table because the table's not made public. However, if you're
-- editing this file's code, the structure is:
--
-- registered_countables[node_name][drop_name] == true
--
-- A single countable node might be able to drop multiple countable
-- drops, so this table structure allows us to account for that fact.
local registered_countables = {}
-- This table is used for reverse lookups. Given an item drop, this
-- table can tell us which keys are needed to look up all forms of that
-- drop that can be counted.
local registered_drops = {}
-- This table keeps track of the multiplier needed to calculate just
-- how many of an item can be stored in the stats table.
local drop_limit_multiplier = {}
-- We need a way to store our data, so let's try this:
local storage = minetest.get_mod_storage()
-- This table is used internally to keep track of the alphabetical
-- order of all registered node/drop pairs.
local sorted_pairs = {}
-- This table is used internally to keep track of the alphabetical
-- order of all registered drops.
local sorted_drops = {}
-- This is the maximum number of items that can be in a Minetest
-- ItemStack (16 bits, unsigned). I know the value off the top of my
-- head because I use it so often, to be honest, but assigning it a
-- variable name makes it more clear to readers of the code why that
-- value is even there. Like, where did 65535 come from? Well, here you
-- go. It's used in a few places due to being the most items that can
-- ever fit into a stack of items in Minetest.
local max_stack_size = 65535
-- For formatting the console output, it's useful to know what the
-- highest node ID and drop ID length are.
local highest_node_ID_length = 0
local highest_drop_ID_length = 0
-- This table contains all the functions registered to run once
-- minestats has been initialised.
local startup_functions = {}
-- This table contains all the functions to be called whenever a stat
-- is updated.
local update_functions = {}
-- This table contains all the functions to be called whenever a
-- level-up occurs.
local levelup_functions = {}
-- A third-party mod may tell Minestats that a particular node drop is
-- countable. The functions registered by mods to check each drop are
-- stored in this table.
local is_countable_callbacks = {}
-- When the determining proficiency level of a player with a given
-- material, that material's own levelling curve needs to be accounted
-- for. In the majority of cases, a material's levelling exponent is
-- 0.414335358827271738046960081192082725465297698974609375. Yes, the
-- exponent is *that* finely tuned. That aside though, not all
-- materials have to have this specific levelling curve, so this table
-- keeps track of the levelling curves of counted materials. It gets
-- populated just before the game begins.
local levelling_exponent = {}
-- The reverse levelling exponent is used in determining how many
-- stacks of a material are needed to get to a specific level, instead
-- of determining what level you are at given a specific number of
-- stacks. It's used to build "next level at" counters.
local reverse_levelling_exponent = {}
-- Like the table above, this table deals with levelling mechanics and
-- is populated just before the game begins. The level limit for each
-- material is usually 99, but again, we shouldn't assume that limit
-- and use this table so we don't have to.
local level_limit = {}
-- Level-calculation's more costly than I'd like it to be. Let's just
-- cache the most expensive-to-compute parts. But also, we want to be
-- able to compare new values to old values to see if there was a
-- change, and the cache lets us do that.
local level_cache = {}
-----------------------------------------------------------------------
------------------------- Internal functions: -------------------------
-----------------------------------------------------------------------
-- Need to know if something's counted? Look no further!
local function is_counted(node_name, drop_name)
if registered_countables[node_name]
and registered_countables[node_name][drop_name] then
return true
else
return false
end
end
-- The storage API uses 32-bit integers, and they wrap. I don't think
-- anyone will ever mine enough of a given drop for it to matter, but
-- we can instead store data as a string to allow higher values. If the
-- number is less than zero, we can assume it wrapped around to get
-- there, not that the player mined a negative number of items, and
-- that the number mined is uncountably high. Lua loses integer
-- precision after 9007199254740992, so that's the point at which we
-- manually wrap. Since it's uncountably high, we'll just return
-- infinity. It makes more sense to do that than return the negative
-- number.
local function get_stat(key)
-- Sometimes tonumber() returns nil instead of a number.
local stat = tonumber(storage:get_string(key)) or 0
-- If the stat is recorded as negative, it's actually uncountably huge.
-- We can't get an exact measure using Lua floats, but we can get sort
-- of close by adding 18014398509481985.
if stat < 0 then
stat = stat + 18014398509481985
end
return stat
end
-- Need to know how many of a given drop a given player has mined?
-- This here's your function.
local function get_pair_stat(player_name, node, drop)
return get_stat(player_name..";"..node..";"..drop)
end
-- This method returns a stat in terms of how many of an item have been
-- harvested, regardless of number dropped in a single dig and
-- regardless of what node dropped it.
local function get_drop_stat(player_name, drop)
local total = 0
if registered_drops[drop] then
for key, multiplier in next, registered_drops[drop] do
total = total + (get_stat(player_name..";"..key) * multiplier)
end
end
return total
end
-- This method returns the number of stacks of the drop a player has
-- mined. It can be used to add some sort of progression to the game
-- from other mods.
local function get_stacks_mined(player_name, drop_name)
local stat = get_drop_stat(player_name, drop_name)
local stack = ItemStack(drop_name)
return math.floor(stat / stack:get_definition().stack_max)
end
-- This method returns the number of engine stacks of the drop a player
-- has mined. It can be used to add some sort of progression to the
-- game from other mods. Engine stacks tend to be far larger than
-- regular stack, so they provide a slower progression for players. For
-- tools, the engine only allows stacks of one. For all other items,
-- the engine allows stacks of (2^16-1).
local function get_engine_stacks_mined(player_name, drop_name)
if minestats.registered_items[drop_name].type == "tool" then
return get_drop_stat(player_name, drop_name)
else
local stat = get_drop_stat(player_name, drop_name)
return math.floor(stat / max_stack_size)
end
end
-- We'll overwrite these functions later in the script (see "External
-- API"), so we need to create local copies of the old versions to call
-- from our new versions.
local builtin_get_node_drops = minetest.get_node_drops
-- This second function needs to be copied after the game begins
-- though, not now. That makes it more likely our version is the
-- outermost version of the function. If ours is not the outermost one,
-- we can't even make sure our version is called at all. Specifically,
-- Minetest Game's creative mod overwrites the node drop handler (iff
-- creative mode is enabled) and doesn't call the previous version of
-- it. If we don't wait to put our own version of the function in
-- place, Minestats will be incompatible with the Minetest Game
-- creative mod.
local builtin_handle_node_drops
-- This method normalises drops so we can better handle them.
-- Sometimes, a node will drop multiple stacks of the same item or will
-- drop unnormalised stacks, such as in the form of legacy item
-- strings. This function lets us properly handle these cases.
local function normalise_drops(drops)
local by_name = {}
local normalised = {}
for _, item in next, drops do
local stack = ItemStack(item)
if not stack:is_empty() then
local count = stack:get_count()
stack:set_count(1)
local name = stack:to_string()
if by_name[name] then
by_name[name] = by_name[name] + count
else
by_name[name] = count
end
end
end
-- We need to return drops in a consistent order for comparison later
-- in the script.
local sorted = {}
for name, _ in next, by_name do
sorted[#sorted+1] = name
end
table.sort(sorted)
for _, name in next, sorted do
local count = by_name[name]
if ItemStack(name):get_definition().type == "tool" then
-- The engine doesn't allow stacking multiple tools, even via code in a
-- mod. Tools just simply cannot be stacked. Ever. If we try to stack
-- the tools, we'll end up deleting all but one of them.
while count > 0 do
normalised[#normalised+1] = name
count = count - 1
end
else
-- A node might drop so many items that we can't store them all in a
-- stack. In practice, this should never happen, but we'd be fools to
-- fail to account for this possibility.
while count > max_stack_size do
normalised[#normalised+1] = ItemStack(name.." "..max_stack_size):to_string()
count = count - max_stack_size
end
end
-- Who knows how many bugs converting to an ItemStack and back fixes?
-- The need for this process was discovered because of the common
-- possibility that an item drop contains a stack of a single item.
-- In the original code, we simply append the size of the stack to the
-- ID of the item. In cases of one item, that's incorrect behaviour and
-- results in unnormalised item strings. We could account for
-- single-item stacks manually, but there's a chance I messed something
-- else up along the way too. Converting to an ItemStack and back
-- ensures that the item string is in the canonically-normalised form
-- as defined by the engine.
--
-- I'm almost certain another bug is present somewhere, too. Flint and
-- cotton seeds from Minetest Game weren't displaying in the stats
-- menu, along with potentially other items. Converting to an ItemStack
-- and back mysteriously fixed that though.
normalised[#normalised+1] = ItemStack(name.." "..count):to_string()
end
return normalised
end
-- Actually solving this problem is incredibly difficult, and I don't
-- know how to do it yet. For now, this function is dummied out.
--
-- Eventually, this function should verify that there could exist a
-- string that matches all patterns and doesn't match any
-- "antipatterns".
local function vague_tool_is_consistent(drop_possibility)
return true
end
-- The next two functions actually both need to be able to call one
-- another, so the second function's variable needs to be declared
-- local before the first function is defined. Otherwise, when calling
-- the second function from within the first, Lua will try and fail to
-- find the function using the global variable name.
local handle_affirmative_drop_state
-- Recursion seems sort of like overkill for the problem at hand, but
-- I can't seem to figure out what else can do the job. Anyway, this
-- function takes a drop table from a node definition and spits out a
-- table of tables, with each inner table representing a list of items
-- that the node could drop if the node is dug. All possibilities are
-- enumerated, but their exact probabilities are not calculated.
local function drop_possibilities(node_drop_table, palette_index, return_table, current_state)
if type(node_drop_table) == "string" then
return_table[1] = {node_drop_table}
elseif not node_drop_table.items then
return_table[1] = {}
else
if current_state.drop_index > #node_drop_table.items
or current_state.number_drops == node_drop_table.max_items then
if current_state.tool
or vague_tool_is_consistent(node_drop_table) then
return_table[#return_table+1] = current_state.drops
end
else
-- This value may be updated before this variable is checked.
local might_not_drop = false
local dropped_state = table.copy(current_state)
dropped_state.drop_index = dropped_state.drop_index + 1
local undropped_state = table.copy(dropped_state)
if node_drop_table.items[current_state.drop_index].tools then
-- If the item definitely drops with the right tool, we have to handle
-- it here. Otherwise, if there's a chance the item wouldn't drop even
-- *with* the right tool, no special treatment is needed. We'll just
-- consider the chance that luck wasn't on the player's side.
if not node_drop_table.items[current_state.drop_index].rarity
or node_drop_table.items[current_state.drop_index].rarity <= 1 then
might_not_drop = true
-- We're holding a specific tool. Compare the tool to the list of tool
-- names and tool name patterns that allow the drop to occur.
if undropped_state.tool then
for _, disallowed_tool in next, node_drop_table.items[current_state.drop_index].tools do
if disallowed_tool:sub(1, 1) == "~" then
if undropped_state.tool:find(disallowed_tool:sub(2)) ~= nil then
might_not_drop = false
end
else
if disallowed_tool == undropped_state.tool then
might_not_drop = false
end
end
end
-- We have limited information on the tool we're holding. Check to see
-- if there are any direct conflicts that would prevent us from missing
-- this drop.
else
-- See if we can miss the drop.
for _, pattern in next, node_drop_table.items[current_state.drop_index].tools do
if undropped_state.toolname_patterns[pattern] then
might_not_drop = false
end
end
-- If we can, add the list of tool patterns that would cause the drop
-- to the list of patterns that don't match the tool we're holding.
if not might_not_drop then
for _, pattern in next, node_drop_table.items[current_state.drop_index].tools do
undropped_state.toolname_antipatterns[pattern] = true
end
end
end
end
end
-- If the item is dropped based on probability, we might not get the
-- drop. It's as simple as that.
if node_drop_table.items[current_state.drop_index].rarity
and node_drop_table.items[current_state.drop_index].rarity > 1 then
might_not_drop = true
end
-- We fork the state here if there's a chance the next set of items
-- might not be dropped.
if might_not_drop then
drop_possibilities(node_drop_table, palette_index, return_table, undropped_state)
end
-- Done forking, let's continue. This branch accounts for if the set of
-- items did drop after all.
dropped_state.number_drops = dropped_state.number_drops + 1
-- First of all, *can* the item drop after all? If we're holding the
-- wrong tool, maybe not.
if node_drop_table.items[current_state.drop_index].tools then
-- If we know what tool we're holding, check it against the list of
-- tool patterns that trigger the drop.
if dropped_state.tool then
local might_drop = false
for _, pattern in next, node_drop_table.items[current_state.drop_index].tools do
if pattern:sub(1, 1) == "~" then
if dropped_state.tool:find(pattern:sub(2)) ~= nil then
might_drop = true
end
else
if pattern == dropped_state.tool then
might_drop = true
end
end
end
if might_drop then
handle_affirmative_drop_state(node_drop_table, palette_index, return_table, dropped_state)
end
-- If we don't know what tool we're holding, compare what would cause
-- the drop with what we know for a fact we don't have.
else
for _, pattern in next, node_drop_table.items[current_state.drop_index].tools do
-- If the pattern is a pattern and not a full tool name, we still don't
-- know what we're holding. Check to see if the pattern directly
-- conflicts with something we know we don't have. If it doesn't, fork,
-- then add it to the list of pattern we know we know our tool matches.
if pattern:sub(1, 1) == "~" then
local might_drop = true
for tool, _ in next, dropped_state.toolname_antipatterns do
if pattern == tool then
might_drop = false
end
end
if might_drop then
local forked_state = table.copy(dropped_state)
forked_state.toolname_patterns[pattern] = true
handle_affirmative_drop_state(node_drop_table, palette_index, return_table, forked_state)
end
-- Finally!! At this point in the script, we don't know for sure what
-- tool we have, but we may or may not have a list of patterns our tool
-- matches against and/or a list of patterns our tool does not match
-- against. Now, we've finally gotten a concrete tool name to match
-- against. Check the tool name against the lists. If the tool name is
-- compatible, fork here, set the tool name, and remove the
-- now-redundant lists.
else
local matches_pattens = true
local does_not_match_antipaterns = true
for tool, _ in next, dropped_state.toolname_patterns do
if pattern:find(tool:sub(2)) == nil then
matches_pattens = false
end
end
for tool, _ in next, dropped_state.toolname_antipatterns do
if tool:sub(1, 1) == "~" then
if pattern:find(tool:sub(2)) ~= nil then
does_not_match_antipaterns = false
end
else
if pattern == tool then
does_not_match_antipaterns = false
end
end
end
if matches_pattens
and does_not_match_antipaterns then
local forked_state = table.copy(dropped_state)
forked_state.toolname_patterns = nil
forked_state.toolname_antipatterns = nil
forked_state.tool = pattern
handle_affirmative_drop_state(node_drop_table, palette_index, return_table, forked_state)
end
end
end
end
else
-- If there are no tool requirements, there's a definite possibility
-- that we can get the drop.
handle_affirmative_drop_state(node_drop_table, palette_index, return_table, dropped_state)
end
end
end
end
-- The function above has two branches that require this logic, and I'd
-- rather not code this same thing twice in case a later Minetest
-- update requires me to modify it, so I made it a separate. Being
-- separate, I added this function call to a third branch as well that
-- otherwise would have hooked into an existing branch using a set
-- variable.
--
-- This function's variable name was already declared local before. If
-- we declare it local a second time, the previous function somehow
-- can't access this function. I think Lua somehow sets up a second
-- local variable with the same name or something, presenting the
-- former variable to functions that bound to the variable before this
-- point and the newer variable to functions that bind to the variable
-- after this point. Very unintuitive. In any case, the easy solution
-- is to just not declare the function name local here, then write a
-- note here in the comments explaining that not declaring the function
-- local was intentional.
function handle_affirmative_drop_state(node_drop_table, palette_index, return_table, dropped_state)
if node_drop_table.items[dropped_state.drop_index-1].inherit_color
and palette_index then
for _, item in next, node_drop_table.items[dropped_state.drop_index-1].items do
local stack = ItemStack(item)
stack:get_meta():set_int("palette_index", palette_index)
dropped_state.drops[#dropped_state.drops+1] = stack:to_string()
end
else
for _, item in next, node_drop_table.items[dropped_state.drop_index-1].items do
dropped_state.drops[#dropped_state.drops+1] = item
end
end
drop_possibilities(node_drop_table, palette_index, return_table, dropped_state)
end
-- Adding a wrapper to that last function will make our API cleaner.
local function node_drop_possibilities(node_name, palette_index)
-- The above function modifies tables passed into it to make recursion
-- a bit more efficient. Instead of building a bunch of tables and
-- having to combine their data, we can just put all the data into a
-- single table to begin with.
--
-- Likewise, the current_state argument is only used internally, so
-- we'll set that as well.
local return_table = {}
local current_state = {
drop_index = 1,
number_drops = 0,
drops = {},
toolname_patterns = {},
toolname_antipatterns = {},
}
drop_possibilities(minetest.registered_nodes[node_name].drop, palette_index, return_table, current_state)
return return_table
end
-- This function calls any functions registered by mods to be called
-- any time a player's stats are updated. This could be used, for
-- example, to build an automatically-updating inventory page for
-- players.
local function call_update_functions(player, drops)
for modname, funct in next, update_functions do
funct(player, drops)
end
end
-- For mods that use Minestats to add a sense of progression to the
-- game, it can be useful to know a player's total level.
local function get_total_level(player_name)
local total_level = 0
for material, partial_level in next, level_cache[player_name] do
total_level = total_level + partial_level
end
return total_level
end
-- This function calls any functions registered by mods to be called
-- any time a player levels up. This could be used, for example, to
-- reward the player in some way or update their abilities.
local function call_levelup_functions(player)
-- First, we'll add up the level, then we'll call the functions and
-- pass that level to them.
local level = get_total_level(player:get_player_name())
for modname, funct in next, levelup_functions do
funct(player, level)
end
end
-- Mods can register update functions to be alerted when a player's
-- stats change.
local function register_update_function(funct)
update_functions[minetest.get_current_modname()] = funct
end
-- Mods can also remove the update functions of other mods, if need be.
local function unregister_update_function(modname)
update_functions[modname] = nil
end
-- Mods can register update functions to be alerted when a player's
-- level changes.
local function register_levelup_function(funct)
levelup_functions[minetest.get_current_modname()] = funct
end
-- Mods can also remove the update functions of other mods, if need be.
local function unregister_levelup_function(modname)
levelup_functions[modname] = nil
end
-- Functions may be registered by other mods for telling Minestats to
-- count more types of drops using this function.
local function register_is_countable(funct)
is_countable_callbacks[minetest.get_current_modname()] = funct
end
-- Functions may be unregistered by other mods so their behaviour can
-- be overridden.
local function unregister_is_countable(modname)
is_countable_callbacks[modname] = nil
end
-- If even one registered function says a drop is valid, this function
-- will reply that the drop is valid.
local function is_countable(node_name, item_string, drop_opts)
if node_name ~= item_string then
for mod_name, funct in next, is_countable_callbacks do
if funct(node_name, item_string, drop_opts) then
return true
end
end
end
return false
end
-- Calculating uses exponents, which is a bit more
-- computationally-expensive than I'd like it to be. As such,
-- get_proficiency_level() just returns data from the cache. When we
-- want to actually recalculate the data, we need to use
-- calculate_proficiency_level() instead.
local function calculate_proficiency_level(player_name, material)
if levelling_exponent[material] then
return math.min(math.floor(get_stacks_mined(player_name, material)^levelling_exponent[material]), level_limit[material])
else
return 0
end
end
-- Given a player name and a material, this function returns a sort of
-- "level" representing that player's experience mining or farming that
-- material. Rather than an outright number of stacks mined, like
-- get_stacks_mined() returns, this function calculates on a curve,
-- requiring players to mine/farm more and more of the material to
-- reach the next level. The maximum level is the maximum stack size
-- for that material (usually 99).
local function get_proficiency_level(player_name, material)
-- We've got a cache, so we should use it.
if level_cache[player_name] then
if level_cache[player_name][material] then
return level_cache[player_name][material]
else
-- if the player is in the cache but the level isn't, that material
-- must not be associated with a stat, and thus not associated with a
-- level. Return zero.
return 0
end
else
-- If the player has been online this server session, their data will
-- be in the cache, but if their data isn't in the cache, we still want
-- this function to return the correct answer. We'll calculate the
-- level and return it.
return calculate_proficiency_level(player_name, material)
end
end
-- Given a player name and a material, this function returns the number
-- of the material left the player needs to mine or farm to reach the
-- next level. If there are no more levels to gain, this function
-- returns zero.
local function next_level_at(player_name, material)
if reverse_levelling_exponent[material] then
local current_level = get_proficiency_level(player_name, material)
if current_level == level_limit[material] then
return 0
else
return math.ceil((current_level + 1)^reverse_levelling_exponent[material]) * level_limit[material]
- get_drop_stat(player_name, material)
end
else
return 0
end
end
-- We want this function to be the last to override
-- minetest.handle_node_drops(). We can define our implementation now,
-- but we can't put it in place until later.
function minestats_handle_node_drops(pos, original_drops, digger)
local update_stats = false
local update_level = false
-- If we have information on what node was dug, let's use it.
-- Otherwise, this function must have been called not by a dig, but by
-- some other mod.
if original_drops.node_dug then
-- Let's normalise the drops for consistency.
local drops = normalise_drops(original_drops)
local player_name = digger:get_player_name()
if registered_countables[original_drops.node_dug] then
for _, item in next, drops do
if registered_countables[original_drops.node_dug][item] then
local key = player_name..";"..original_drops.node_dug..";"..item
local previous_value = tonumber(storage:get_string(key)) or 0
-- At this point, we manually wrap the number because Lua loses integer
-- precision and we'd otherwise just repeat the same integer forever.
if previous_value == 9007199254740992 then
storage:set_string(key, "-9007199254740992")
else
local new_value = string.format("%d", previous_value+1)
storage:set_string(key, new_value)
end
update_stats = true
local material_name = ItemStack(item):get_name()
local new_level = calculate_proficiency_level(player_name, material_name)
if new_level ~= level_cache[player_name][material_name] then
-- We have the new level calculated. We should update the cache at this
-- point so we don't need to recalculate and update later.
level_cache[player_name][material_name] = new_level
update_level = true
end
end
end
end
end
-- Whether called by a dig or by another mod, we should call the
-- default implementation to finish up.
builtin_handle_node_drops(pos, original_drops, digger)
-- Is stats have actually changed, we should notify mods that have
-- requested to be notified.
if update_stats then
-- First, notify mods about changed stats.
call_update_functions(digger, original_drops)
-- Then, if the level has changed as well, notify mods about that as
-- well.
if update_level then
call_levelup_functions(digger)
end
end
end
-- This just returns a list of registered countables that other mods
-- can use. PLEASE NOTE: The node/drop pair list isn't built up until
-- after the game starts, as other mods might not be done registering
-- nodes and items until then. If called during load time, this
-- function will always return an empty table. This function is
-- therefore only useful at run time.
local function get_all_pairs()
return table.copy(sorted_pairs)
end
-- Again, this function returns an empty table if called at load time.
-- Wait until run time to call it.
local function get_all_drops()
return table.copy(sorted_drops)
end
-- Retrieve the drop limit multiplier for a given drop.
function get_drop_limit_multiplier(drop_name)
return drop_limit_multiplier[drop_name]
end
-- Sometimes, a mod may need code to run when the game begins, but
-- after minestats has initialised. Using minetest.after() is no
-- guarantee unless you pass it a time longer than zero seconds.
-- Instead of registering a function with minetest.after, you can
-- register one with this function and it will run as soon as the
-- minestats API is fully functional.
local function register_startup_function(funct)
startup_functions[minetest.get_current_modname()] = funct
end
-- A "node definition" might actually be several node definitions
-- rolled into one.
local function node_forms(node_name)
local palette = minetest.registered_nodes[node_name].palette
local paramtype2 = minetest.registered_nodes[node_name].paramtype2
local increment
if paramtype2 == "color" then
increment = 1
elseif paramtype2 == "colorwallmounted" then
increment = 8
elseif paramtype2 == "colorfacedir" then
increment = 32
end
if palette and increment then
local array = {}
local index = 0
while index < 256 do
array[#array+1] = {string=node_name..' 1 0 "\\0001palette_index\\0002'..index..'\\0003"', palette_index=index}
index = index + increment
end
return array
else
return {{string=node_name}}
end
end
-----------------------------------------------------------------------
---------------------------- External API: ----------------------------
-----------------------------------------------------------------------
-- To grant drop points to the player, we need to know which player
-- dug the node, what node was dug, and what item was dropped. The
-- Minetest API doesn't give us all that information at once. Instead,
-- it tells us what node was dug when we need to find the drops, then
-- tells us the drops and tells us what player they're for. In order to
-- get all the information we need, we need to sneak the dug node's
-- name into the drop table.
function minetest.get_node_drops(node, tool_name)
local drops = builtin_get_node_drops(node, tool_name)
-- When a player digs, we get a chance to remove this item from the
-- drop list before passing it to Minetest's handlers. When drops are
-- gotten for some other reason, we don't. If we add the information on
-- what node was dug by simply setting this value to the name of the
-- node, it causes item duplication in some cases (for example, when
-- attached nodes fall). However, if we store this value in a meta
-- table, we can retrieve the value later without Minetest doing
-- anything silly with it in the mean time.
local paramtype2 = ItemStack(node.name):get_definition().paramtype2
local palette_index = minetest.strip_param2_color(node.param2, paramtype2)
if palette_index then
setmetatable(drops, {__index = {node_dug = node.name..' 1 0 "\\0001palette_index\\0002'..palette_index..'\\0003"'}})
else
setmetatable(drops, {__index = {node_dug = node.name}})
end
return drops
end
minetest.register_on_mods_loaded(function()
-- After all mods have loaded, we need to go through the node table to
-- figure out which countable node/drop pairs have been defined in the
-- game.
for node_name, def in next, minetest.registered_nodes do
-- No drop property means the node drops itself. It's not countable.
if def.drop
-- If the node's not pointable, how are we supposed to mine it?
and minetest.registered_nodes[node_name].pointable
-- If the node is not diggable, we shouldn't be tormenting players with
-- a visible stat in their menus that they'll never be able to raise.
and def.diggable
-- If the can_dig() method is defined, the node either can never be dug
-- (so again, we shouldn't torment players) of the node is likely some
-- unnatural node such as a furnace or chest. We shouldn't be keeping
-- stats for these kinds of nodes.
and not def.can_dig then
-- For now, node forms just refers to the different versions of
-- coloured nodes. If some other type of node variant needs to be
-- accounted for later, that can be added in as well.
for _, form in next, node_forms(node_name) do
local possibilities = node_drop_possibilities(node_name, form.palette_index)
local may_be_countable = {}
local blacklisted = {}
for _, possibility in next, possibilities do
possibility = normalise_drops(possibility)
for _, stack_string in next, possibility do
may_be_countable[stack_string] = true
end
end
-- If one of the recipes matches one of the drops, that drop is
-- basically just the node dropping itself in another form. Nodes that
-- do this include stone, which drops cobble that can be smelted back
-- into stone, and clay blocks, which drop four clay lumps that can be
-- crafted back into one clay block.
local recipes = minetest.get_all_craft_recipes(node_name)
if recipes then
for _, recipe in next, recipes do
local ingredients = normalise_drops(recipe.items)
for _, ingredient in next, ingredients do
blacklisted[ingredient] = true
end
end
end
for item, _ in next, may_be_countable do
local item_ID = ItemStack(item):get_name()
if not blacklisted[item]
and is_countable(form.string, item, possibilities) then
if not registered_countables[form.string] then
registered_countables[form.string] = {}
end
registered_countables[form.string][item] = true
local node_length = string.len(form.string)
if node_length > highest_node_ID_length then
highest_node_ID_length = node_length
end
local item_length = string.len(item)
if item_length > highest_drop_ID_length then
highest_drop_ID_length = item_length
end
end
end
end
end
end
-- Here, we set up several important tables. It's quicker to work with
-- them all at once instead of building them separately, as the
-- server doesn't have to cycle through the drop materials as many
-- times. First, we have the registered_drops table, which is used for
-- reverse lookups, as well as the sorted_drops drops table, of which a
-- copy of is given to any mod that requests it. While we're building
-- these tables, we also take the opportunity to build the
-- levelling_exponent table, used to provide a levelling mechanic in
-- which reaching the next level requires exponentially higher numbers
-- of drops, and the drop_limit_multiplier table, used to determine
-- just how many drops of a material could theoretically be kept track
-- of under perfect conditions that can never actually be met in
-- practice. It's okay though. Even the lowest limit is higher than
-- will ever be reached in practice. We also build the level_limit
-- table here, used to put a hard cap on how high levels can go.
local denominator = math.log(max_stack_size)
for node_name, drop_names in next, registered_countables do
for drop_name, _ in next, drop_names do
sorted_pairs[#sorted_pairs+1] = {
node = node_name,
drop = drop_name,
}
local itemstack = ItemStack(drop_name)
local count = itemstack:get_count()
itemstack:set_count(1)
local drop_key = itemstack:to_string()
local key = node_name..";"..drop_name
if registered_drops[drop_key] then
registered_drops[drop_key][key] = count
drop_limit_multiplier[drop_key] = drop_limit_multiplier[drop_key] + registered_drops[drop_key][key]
else
registered_drops[drop_key] = {
[key] = count,
}
sorted_drops[#sorted_drops+1] = drop_key
drop_limit_multiplier[drop_key] = registered_drops[drop_key][key]
if minetest.registered_items[itemstack:get_name()] then
level_limit[drop_key] = minetest.registered_items[itemstack:get_name()].stack_max
else
level_limit[drop_key] = minetest.registered_items.unknown.stack_max
end
local numerator = math.log(level_limit[drop_key])
levelling_exponent[drop_key] = numerator/denominator
reverse_levelling_exponent[drop_key] = denominator/numerator
end
end
end
table.sort(sorted_pairs, function(pair_0, pair_1)
if pair_0.node == pair_1.node then
return pair_0.drop < pair_1.drop
else
return pair_0.node < pair_1.node
end
end)
table.sort(sorted_drops)
-- It's time! Let's put our implementation of the drop handler in
-- place.
builtin_handle_node_drops = minetest.handle_node_drops
minetest.handle_node_drops = minestats_handle_node_drops
-- It's too late now to register startup functions. These should be
-- registered at load time, not run time. Let's remove this API
-- function now.
__minestats__.register_startup_function = nil
-- We're now completely done finding drop possibilities to count for
-- stats. It's too late to register a new handler for that. We can also
-- delete the registered handlers while we're at it, as we're done with
-- them.
__minestats__.register_is_countable = nil
is_countable_callbacks = nil
-- It's also too late to register update functions, but this is mostly
-- because of the way update function registration is implemented.
__minestats__.register_update_function = nil
-- The minestats API is now fully functional. Let's run the functions
-- other mods have requested we run.
for modname, funct in next, startup_functions do
funct()
end
-- We've already run the startup functions. We can now remove them.
startup_functions = nil
end)
-- A debug command; useful for some, but ignorable for most.
minetest.register_chatcommand("minestats", {
description = "Output mining statistics",
func = function(name)
minetest.chat_send_player(name, name.."'s mining statistics:")
local last_node = ""
for _, pair in next, sorted_pairs do
local node = ""
if pair.node ~= last_node then
node = pair.node
end
minetest.chat_send_player(name, string.format("%-"..highest_node_ID_length.."s => %-"..highest_drop_ID_length.."s : %10d", node, pair.drop, get_pair_stat(name, pair.node, pair.drop)))
last_node = pair.node
end
return true, "END OF MINESTATS OUTPUT"
end,
})
-- Calculating a player's full level all at once is more expensive than
-- I'd like it to be, not to mention that with the new on-level-up
-- callback feature, a player's level has to be recalculated *every
-- time* one of their stats is raised so as to see if the level has
-- changed. We greatly alleviate the problem by using a cache to store
-- the current proficiency levels, which we can initialise when the
-- player logs on for the first time after a server restart. From
-- there, we only have to recalculate the parts of the player's level
-- referring to individual stats that changed and compare them to the
-- previous values for those components. If there's a change, we can
-- use the new values along with even more data from the cache to
-- calculate the new total level and call any registered on-level-up
-- callbacks.
minetest.register_on_joinplayer(function(player)
local player_name = player:get_player_name()
if not level_cache[player_name] then
level_cache[player_name] = {}
for material, _ in next, registered_drops do
level_cache[player_name][material] = calculate_proficiency_level(player_name, material)
end
end
end)
-- Make the API visible to other mods:
__minestats__ = {
get_all_drops = get_all_drops ,
get_all_pairs = get_all_pairs ,
get_drop_stat = get_drop_stat ,
get_drop_limit_multiplier = get_drop_limit_multiplier ,
get_engine_stacks_mined = get_engine_stacks_mined ,
get_pair_stat = get_pair_stat ,
get_proficiency_level = get_proficiency_level ,
get_stacks_mined = get_stacks_mined ,
get_total_level = get_total_level ,
is_counted = is_counted ,
next_level_at = next_level_at ,
register_is_countable = register_is_countable ,
register_levelup_function = register_levelup_function ,
register_startup_function = register_startup_function ,
register_update_function = register_update_function ,
unregister_is_countable = unregister_is_countable ,
unregister_levelup_function = unregister_levelup_function,
unregister_update_function = unregister_update_function ,
}
-----------------------------------------------------------------------
-------------------------- Deprecated Stuff: --------------------------
-----------------------------------------------------------------------
-- When Minetest starts, data will be updated from the old hybrid/pure
-- stat split form to the new unified form. Hybrid-specific API methods
-- are no longer valid, as there are no hybrid stats. Because there are
-- no hybrid stats, it doesn't make sense to actually check for
-- anything with the old method calls, so the old method calls have
-- been replaced with simpler, dummy methods that would return the same
-- values as the original API methods would have under the new
-- conditions anyway. If you are using these method calls in your code
-- still, please replace them with more modern API calls.
--
-- Deprecated on 2018-02-06; DO NOT REMOVE FOR AT LEAST TWO YEARS.
function __minestats__.get_hybrid_stat()
return 0
end
function __minestats__.is_hybrid()
return false
end
__minestats__.get_stat = __minestats__.get_pair_stat
startup_functions["*DEPRECATED 2018-02-06*"] = function()
local storage_table = storage:to_table()
for _, player_name in next, minetest.get_dir_list(minetest.get_worldpath().."/players", false) do
for _, pair in next, sorted_pairs do
local old_key = player_name..";"..pair.drop
if storage_table.fields[old_key] then
local new_key = player_name..";"..pair.node..";"..pair.drop
if storage_table.fields[new_key] then
storage:set_string(new_key, storage_table.fields[new_key] + storage_table.fields[old_key])
else
storage:set_string(new_key, storage_table.fields[old_key])
end
storage:set_string(old_key)
end
end
end
end
-- When the breakthrough in Minestats was made that allowed farm seeds
-- and saplings to be counted, the term "mineral" no longer made sense
-- in the context of Minestats. For future usability and to make the
-- API make sense, all references to minerals were removed from the
-- API's method names. These aliases have been put in place to avoid
-- breaking mods that depend on Minestats, but they'll be removed in a
-- couple years. Please update your mods accordingly.
--
-- Deprecated on 2018-03-17; DO NOT REMOVE FOR AT LEAST TWO YEARS.
__minestats__.get_all_minerals = __minestats__.get_all_pairs
__minestats__.get_mineral_multiplier = __minestats__.get_drop_limit_multiplier
__minestats__.get_mineral_stat = __minestats__.get_pair_stat
__minestats__.is_mineral = __minestats__.is_counted
-- When Minetest starts, the database will be purged of all the
-- useless, playerless stats.
--
-- Deprecated on 2018-04-07; DO NOT REMOVE FOR AT LEAST TWO YEARS.
startup_functions["*DEPRECATED 2018-04-07*"] = function()
local storage_table = storage:to_table()
for key, value in next, storage_table.fields do
if key:sub(1, 1) == ";" then
storage:set_string(key)
end
end
end