1403 lines
60 KiB
Lua
1403 lines
60 KiB
Lua
-- liblevelup mod for Minetest
|
|
-- Copyright © 2017-2021 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/>.
|
|
|
|
-----------------------------------------------------------------------
|
|
----------------- Deprecated mod name database import -----------------
|
|
-----------------------------------------------------------------------
|
|
|
|
-- liblevelup used to be called "minestats". If the older database
|
|
-- under the deprecated name still exists, we should import that to the
|
|
-- new database. Usually, I keep all code related to supporting
|
|
-- deprecated features at the end of the file, but this database import
|
|
-- has to come before everything else. Otherwise, when liblevelup
|
|
-- requests its storage object, the data the storage object contains
|
|
-- will be incorrect.
|
|
--
|
|
-- Legacy support in this mod is only enabled when overall Minetest
|
|
-- legacy support is enabled in minetest.conf. By default, legacy
|
|
-- support is enabled for release versions but not development
|
|
-- versions.
|
|
--
|
|
-- Deprecated on 2020-02-29; DO NOT REMOVE FOR AT LEAST TWO YEARS.
|
|
if minetest.settings:get("deprecated_lua_api_handling") ~= "error" then
|
|
local liblevelup_database = io.open(minetest.get_worldpath().."/mod_storage/liblevelup", "r")
|
|
if liblevelup_database then
|
|
liblevelup_database:close()
|
|
else
|
|
local minestats_database = io.open(minetest.get_worldpath().."/mod_storage/minestats", "r")
|
|
if minestats_database then
|
|
local liblevelup_database = io.open(minetest.get_worldpath().."/mod_storage/liblevelup", "w")
|
|
liblevelup_database:write(minestats_database:read())
|
|
liblevelup_database:close()
|
|
minestats_database:close()
|
|
end
|
|
end
|
|
end
|
|
|
|
-----------------------------------------------------------------------
|
|
------------------------- 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_key(drop_list)] == 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 = {}
|
|
|
|
-- 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 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
|
|
|
|
-- This table contains all the functions registered to run once
|
|
-- liblevelup 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 liblevelup 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 the maximum stack size
|
|
-- 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 drop_max_stack_size = {}
|
|
|
|
-- 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 unscaled_level_cache = {}
|
|
|
|
-- The scaled levels are pretty costly too, and the full set of scaled
|
|
-- levels have to be calculated every single time the player collects a
|
|
-- drop from a node even if that node only dropped something related
|
|
-- to one of the levels. We might as well cache the levels at that time
|
|
-- to avoid recalculating them when other mods query these levels.
|
|
local scaled_level_cache = {}
|
|
|
|
-- If a craft-predicting function returns an empty ItemStack, Minetest
|
|
-- now disables the craft. This means we can prevent items from being
|
|
-- crafted based on arbitrary criteria. The keys in this table
|
|
-- represent items that the player isn't allowed to craft unless
|
|
-- they've met certain level requirements, and the values are tables of
|
|
-- level requirements with the drop the player must have levels in
|
|
-- being the keys and the level in that particular drop being the
|
|
-- values.
|
|
local craft_locks = {}
|
|
|
|
-- In implementing craft locks, we send messages to players that
|
|
-- haven't met the unlocking requirements, so we should probably be
|
|
-- sending translated strings.
|
|
local S = minetest.get_translator("liblevelup")
|
|
|
|
-----------------------------------------------------------------------
|
|
------------------------- Internal functions: -------------------------
|
|
-----------------------------------------------------------------------
|
|
|
|
-- 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
|
|
|
|
-- Checking to see if a given list of items would be counted as a drop
|
|
-- requires putting the entire drop list into a single string to check
|
|
-- against a table.
|
|
local function drop_key(drop_list)
|
|
-- Multiple stacks in the same drop list might be the same type of
|
|
-- item, which should be treated the same as if the same quantity of
|
|
-- items in the drop list were present but as a single stack. Stack
|
|
-- order also shouldn't influence how drop lists are treated, so we
|
|
-- need to sort the drop list. Normalisation of the drop list takes
|
|
-- care of both.
|
|
local drops = normalise_drops(drop_list)
|
|
-- Te list could be empty. If so, the list isn't counted. Return an
|
|
-- empty string anyway instead of nil to avoid errors caused by
|
|
-- assuming the return value of this function is a string. If not,
|
|
-- build the table key needed to check.
|
|
if drops[1] then
|
|
local key = drops[1]
|
|
if drops[2] then
|
|
for i = 2, #drops do
|
|
key = key..";"..drops[i]
|
|
end
|
|
end
|
|
return key
|
|
else
|
|
return ""
|
|
end
|
|
end
|
|
|
|
-- Need to know if something's counted? Look no further! Keep in mind
|
|
-- that drops are counted based on context, so the full drop list needs
|
|
-- to be passed to this function.
|
|
local function is_counted(node_name, drop_list)
|
|
local key = drop_key(drop_list)
|
|
if registered_countables[node_name]
|
|
and registered_countables[node_name][key] then
|
|
return true
|
|
else
|
|
return false
|
|
end
|
|
end
|
|
|
|
-- This method simply gets a string from the database and casts it to
|
|
-- an integer. All other functions that get data from the database
|
|
-- aren't interacting with the database directly, but are merely
|
|
-- calling this function and using the return value in some way.
|
|
local function get_stat(key)
|
|
-- Sometimes tonumber() returns nil instead of a number.
|
|
return tonumber(storage:get_string(key)) or 0
|
|
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)
|
|
return get_stat(player_name..";"..drop)
|
|
end
|
|
|
|
-- This method returns the number of stacks of the drop a player has
|
|
-- mined. Fractional values are returned, to allow for levels to
|
|
-- account for everything a player has mined in a unified way.
|
|
local function get_stacks_mined(player_name, drop_name)
|
|
return get_drop_stat(player_name, drop_name) / drop_max_stack_size[drop_name]
|
|
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, liblevelup will be incompatible with the Minetest Game
|
|
-- creative mod.
|
|
local builtin_handle_node_drops
|
|
|
|
-- 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 function.
|
|
-- 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
|
|
|
|
-- Calculating uses exponents, which is a bit more
|
|
-- computationally-expensive than I'd like it to be. As such,
|
|
-- get_unscaled_partial_level() just returns data from the cache. When
|
|
-- we want to actually recalculate the data, we need to use
|
|
-- calculate_unscaled_partial_level() instead.
|
|
local function calculate_unscaled_partial_level(player_name, material)
|
|
if levelling_exponent[material] then
|
|
return math.min(get_stacks_mined(player_name, material)^levelling_exponent[material], max_stack_size)
|
|
else
|
|
return 0
|
|
end
|
|
end
|
|
|
|
-- This just conveniently adds up the unscaled levels, which needs to
|
|
-- be done in order to calculate the scaled level or calculate the size
|
|
-- of the level progress bar.
|
|
local function get_unscaled_floating_point_level(player_name)
|
|
local total_level = 0
|
|
for material, partial_level in next, unscaled_level_cache[player_name] do
|
|
total_level = total_level + partial_level
|
|
end
|
|
return total_level
|
|
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.
|
|
local function get_unscaled_partial_level(player_name, material)
|
|
-- We've got a cache, so we should use it.
|
|
if unscaled_level_cache[player_name] then
|
|
if unscaled_level_cache[player_name][material] then
|
|
return unscaled_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_unscaled_partial_level(player_name, material)
|
|
end
|
|
end
|
|
|
|
-- The first time that a player's level is queried, this function needs
|
|
-- to be called to calculate all of their levels. Each time the player
|
|
-- gets a drop, this function needs to be called again to calculate all
|
|
-- the levels and see if the player levelled up and the level-up
|
|
-- callbacks need to be called.
|
|
local function calculate_scaled_partial_levels(player_name)
|
|
local raw_value = {}
|
|
for _, drop in next, sorted_drops do
|
|
raw_value[drop] = math.sqrt(
|
|
get_unscaled_partial_level(player_name, drop) * (get_unscaled_floating_point_level(player_name) / #sorted_drops)
|
|
)
|
|
end
|
|
rawset(scaled_level_cache, player_name, raw_value)
|
|
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 the first
|
|
-- time the data is needed or the first time the data changes,
|
|
-- whichever happens first. 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.
|
|
--
|
|
-- Previously, the generation of cached data was done when a player
|
|
-- logged in. However, I found that sometimes, a player's level might
|
|
-- get checked even if that player hasn't logged in since the last
|
|
-- restart. One example of this is if a sfinv page displays a player's
|
|
-- level. In one case, I found that sfinv was generated before the
|
|
-- registered on_join function from this mod was run, causing the game
|
|
-- to crash because the sfinv page was trying to access data from
|
|
-- liblevelup that wasn't yet available but would be a fraction of a
|
|
-- second later when the on_join function from this mod would get run.
|
|
-- This is only one example though. A mod should be able to check the
|
|
-- level of absolutely any player without fear of crashing the game and
|
|
-- without getting a zeroed-out result. To handle that, the
|
|
-- cache-generation functionality was moved to this metatable. The
|
|
-- first time that the data is needed, it'll be generated.
|
|
setmetatable(unscaled_level_cache, {
|
|
__index = function(unscaled_level_cache, player_name)
|
|
local raw_value = rawget(unscaled_level_cache, player_name)
|
|
if not raw_value then
|
|
raw_value = {}
|
|
for _, material in next, sorted_drops do
|
|
raw_value[material] = calculate_unscaled_partial_level(player_name, material)
|
|
end
|
|
rawset(unscaled_level_cache, player_name, raw_value)
|
|
end
|
|
return raw_value
|
|
end
|
|
})
|
|
|
|
-- Same logic as above. If the data is queried but hasn't been
|
|
-- calculated, let's calculate it.
|
|
setmetatable(scaled_level_cache, {
|
|
__index = function(scaled_level_cache, player_name)
|
|
local raw_value = rawget(scaled_level_cache, player_name)
|
|
if not raw_value then
|
|
calculate_scaled_partial_levels(player_name)
|
|
raw_value = rawget(scaled_level_cache, player_name)
|
|
end
|
|
return raw_value
|
|
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 liblevelup 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
|
|
|
|
-- Mods can register craft locks, which prevent items from being
|
|
-- crafted if level requirements have not been met. Aliases, item
|
|
-- groups, and item quantities are not handles here. Only specific
|
|
-- known item names should be registered. If multiple locks on the same
|
|
-- item are registered - for example, by different mods - they'll be
|
|
-- merged and the level requirements for all registered locks will need
|
|
-- to be met in order to craft the item.
|
|
local function register_craft_lock(name, requirements)
|
|
craft_locks[name] = craft_locks[name] or {}
|
|
for drop, level in next, requirements do
|
|
-- We attach "or level" here to the existing value because a value
|
|
-- might not yet be registered, at which point the level will simply be
|
|
-- compared to itself by math.max().
|
|
craft_locks[name][drop] = math.max(craft_locks[name][drop] or level, level)
|
|
end
|
|
end
|
|
|
|
-- All requirements for unlocking the craft will be removed, and the
|
|
-- player will have that craft available to them from the start.
|
|
local function remove_craft_lock(name)
|
|
craft_locks[name] = 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, drop_list, drop_opts, drop_key)
|
|
drop_list = normalise_drops(drop_list)
|
|
for mod_name, funct in next, is_countable_callbacks do
|
|
if funct(node_name, drop_list, drop_opts, drop_key) then
|
|
return true
|
|
end
|
|
end
|
|
return false
|
|
end
|
|
|
|
-- Mods developers should really think about whether they actually need
|
|
-- per-material player levels before making use of this feature. In
|
|
-- most cases, a unified level would be a much cleaner experience for
|
|
-- players. In fact, I originally planned not to provide a per-material
|
|
-- level-retrieval function specifically so it wouldn't be misused.
|
|
-- However, there are some cases in which per-material levels make
|
|
-- sense. I'm not saying not to use this function, I'm just saying you
|
|
-- should consider unified levels first and make sure you've ruled them
|
|
-- out for a good reason before resorting to this feature.
|
|
--
|
|
-- To make this function much more balanced, it heavily takes into
|
|
-- account a the average drop counts across all possible drops. As a
|
|
-- result, players with a low overall level will always have low
|
|
-- per-material levels even if they've maxed out the amount of that
|
|
-- material that will affect levels. While this doesn't fully alleviate
|
|
-- the concerns expressed in the paragraph above, it does greatly
|
|
-- reduce them.
|
|
local function get_scaled_per_material_player_level(player_name, drop)
|
|
local level = scaled_level_cache[player_name][drop] or 0
|
|
local cap = drop_max_stack_size[drop] or 0
|
|
return math.min(cap, math.floor(level))
|
|
end
|
|
|
|
-- For mods that use liblevelup to add a sense of progression to the
|
|
-- game, it can be useful to know a player's level.
|
|
local function get_scaled_player_level(player_name)
|
|
local level = 0
|
|
for drop_name, drop_level in next, scaled_level_cache[player_name] do
|
|
level = level + math.min(drop_max_stack_size[drop_name], math.floor(drop_level))
|
|
end
|
|
return level
|
|
end
|
|
|
|
-- I was running into a lot of bugs in the code related to next level
|
|
-- meters, and I wasn't sure how much of the issue is caused by
|
|
-- mistaken inconsistency in the code, so I just dumped all the
|
|
-- relevant code into this function. The other two functions can call
|
|
-- this one as need be, and I can be sure that they're both doing the
|
|
-- same thing as one another. It turned out inconsistency had nothing
|
|
-- to do with the issue, but this still makes the code easier to debug.
|
|
local function get_per_level_material_scaled_level_at(player_name, material, target_level)
|
|
-- Due to precision errors, the exact starting point isn't getting
|
|
-- accurately determined. When at level zero, this can cause this
|
|
-- extrapolation algorithm to predict that the current level would have
|
|
-- been reached a drop before or after it really would be. I have no
|
|
-- idea what to do about that; there's no way to have infinite
|
|
-- precision, so we can't eliminate precision errors. I think most of
|
|
-- the time, the effect is unnoticeable, especially because of the
|
|
-- ever-shifting denominators that result from the complexities of the
|
|
-- levelling system. Importantly, this rounding issue affects only
|
|
-- level predictions (and not even the goal state, but the extrapolated
|
|
-- starting state), and not the actual level of the player. However,
|
|
-- there's a corner case: level zero. There are two odd behaviours that
|
|
-- are *highly* noticeable for players at level zero. The most obvious
|
|
-- error is when -nan is is returned instead of the valid starting
|
|
-- point of zero drops. This seems to happen when precision errors set
|
|
-- the extrapolated starting point to be below zero. The second is when
|
|
-- the extrapolated starting point is instead estimated to be one
|
|
-- instead of zero. This sets causes progress pages, for example, to
|
|
-- say the progress toward the next level - level 1 - is one less than
|
|
-- the total number of drops obtained. It makes absolutely no sense to
|
|
-- the player and only serves to confuse them. I'm utterly confused as
|
|
-- to even one semantically correct thing I can try in attempting to
|
|
-- resolve the issue.
|
|
--
|
|
-- Out of desperation, I'm just checking for level zero, and if level
|
|
-- zero isn't the case, having liblevelup actually do the maths. This
|
|
-- results in correct return values for players at level zero and
|
|
-- almost-correct values for everyone else.
|
|
if target_level == 0 then
|
|
return 0
|
|
elseif reverse_levelling_exponent[material] then
|
|
local current_material_level = get_unscaled_partial_level(player_name, material)
|
|
if current_material_level == drop_max_stack_size[material] then
|
|
return math.huge
|
|
else
|
|
local target = target_level^2
|
|
local average_material_level = get_unscaled_floating_point_level(player_name) / #sorted_drops
|
|
-- BEGIN COMPLEX, UNVERIFIED MATHS
|
|
--
|
|
-- This part's a bit above me. Basically, the intent is that this
|
|
-- equality be true:
|
|
--
|
|
-- (current_material_level + X) * (average_material_level + X / #sorted_drops) == target
|
|
--
|
|
-- We then figure out how many of the drop is needed to get to level
|
|
-- (current_material_level + X) and call it a day. I couldn't figure
|
|
-- out where to go after I'd expressed my intent in that form though. I
|
|
-- ended up asking an algebra solver hoping that I could walk through
|
|
-- the steps it provided an arrive at the same solution it did, but it
|
|
-- solved it using quadratic equations and at the moment, I'm far too
|
|
-- stressed out by other things in my life to keep the variables
|
|
-- straight in my head enough to work with quadratic equations. When
|
|
-- I'm in a better place mentally and emotionally, I need to come back
|
|
-- and verify that this equation actually does what it's supposed to.
|
|
-- Until then, I'm just going to trust that the computerised algebra
|
|
-- solver knows what it's doing and that this equation does actually
|
|
-- solve for X so I can continue development.
|
|
local X = (
|
|
math.sqrt(
|
|
4 * #sorted_drops * target
|
|
- 2 * current_material_level * average_material_level * #sorted_drops
|
|
+ average_material_level^2 * (#sorted_drops)^2
|
|
+ current_material_level^2
|
|
)
|
|
- current_material_level
|
|
- average_material_level * #sorted_drops
|
|
) / 2
|
|
-- END COMPLEX, UNVERIFIED MATHS
|
|
local target_material_level = current_material_level + X
|
|
return math.ceil(target_material_level^reverse_levelling_exponent[material] * drop_max_stack_size[material])
|
|
end
|
|
else
|
|
return math.huge
|
|
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 per-material level. If there are no more levels to gain, this
|
|
-- function returns infinity. Consider using next_scaled_level_at()
|
|
-- instead, when reasonable.
|
|
local function next_level_at(player_name, material)
|
|
return get_per_level_material_scaled_level_at(player_name, material, math.floor(scaled_level_cache[player_name][material] or 0) + 1)
|
|
- get_drop_stat(player_name, material)
|
|
end
|
|
|
|
-- This helper function determines the length of a player's progress
|
|
-- progress bar for things such as level-up progress bars. In
|
|
-- combination with the next_level_at(), it can be used to determine
|
|
-- what a user's progress bar percentage and progress bar text should
|
|
-- be set to.
|
|
--
|
|
-- progress_percent == (length - next_scaled_level_at) / length
|
|
local function player_material_progress_bar_length(player_name, material)
|
|
local current_level = math.floor(scaled_level_cache[player_name][material] or 0)
|
|
return get_per_level_material_scaled_level_at(player_name, material, current_level + 1)
|
|
- get_per_level_material_scaled_level_at(player_name, material, current_level)
|
|
end
|
|
|
|
-- Incrementing is handled differently depending on a setting that is
|
|
-- read only when the game starts.
|
|
local increment
|
|
|
|
-- If unlimited counting is enabled, we count on and on without end and
|
|
-- without overflow. In this case, we work with the number fifteen
|
|
-- digits at a time, which allows us to work not at the integer
|
|
-- precision limit, but instead at the RAM limit.
|
|
--
|
|
-- If we weren't using the default Minetest implementation by default,
|
|
-- and thus didn't need the default Minetest implementation to be
|
|
-- compatible with our own implementation, we could save space by using
|
|
-- hexadecimal instead of decimal. The default Minetest implementation
|
|
-- uses decimal though, so we must use decimal too. It doesn't actually
|
|
-- matter though. You shouldn't need to even turn on our
|
|
-- implementation, so the default Minetest implementation is the one
|
|
-- that matters here.
|
|
if Settings(minetest.get_worldpath().."/world.conf"):get_bool("liblevelup.enable_infinite_counter", false) or true then
|
|
function increment(key, quantity)
|
|
local count = storage:get_string(key)
|
|
local continue = true
|
|
local offset = 0
|
|
while continue do
|
|
local prefix = count:sub(1, offset - 16)
|
|
-- I'm not sure how to correctly set the suffix without a bizarre
|
|
-- exception for the first working segment. I guess for now, a bizarre
|
|
-- exception is going to have to do.
|
|
--
|
|
-- To sum up the situation simply, the problem seems to be that I have
|
|
-- no idea what input would yield an empty string when no suffix is
|
|
-- available.
|
|
local suffix = ""
|
|
if offset ~= 0 then
|
|
suffix = count:sub(offset)
|
|
end
|
|
local working_segment = count:sub(offset - 15, offset - 1)
|
|
-- An empty segment can't appear in the middle of a string, so if the
|
|
-- segment is empty, we can ignore the prefix, which must also be
|
|
-- empty. We also need to treat the empty segment as zero, but there's
|
|
-- no need to set it to zero and increment, because incrementing zero
|
|
-- will always yield the value we incremented by. So we skip right to
|
|
-- using the value we're adding as the final value, and append the
|
|
-- suffix to it. Here, we terminate the loop, as we've finished the
|
|
-- incrementing.
|
|
if working_segment == "" then
|
|
count = quantity .. suffix
|
|
continue = false
|
|
-- if the segment isn't empty, we need to add the value to it.
|
|
else
|
|
working_segment = working_segment + quantity
|
|
-- If there's no prefix, we're working with the most-significant
|
|
-- segment and can just write it as-is. Overflow, if any, into the next
|
|
-- segment can just be dealt with by writing this segment longer than
|
|
-- it should be. As a side note, a more-robust check would be needed if
|
|
-- arbitrarily-large values could be added, but in fact, the value
|
|
-- added will never be more than the maximum stack size of 65535, so
|
|
-- the value to carry to the next segment, if any, will always be
|
|
-- exactly one. No need for a special check.
|
|
if prefix == "" then
|
|
count = prefix .. string.format("%d", working_segment) .. suffix
|
|
continue = false
|
|
-- If the segment is maxed out, we need to set it back to all zeros and
|
|
-- increment the next segment by one. Here, we change the offset, but
|
|
-- the incrementing hasn't completed yet, so the loop continues.
|
|
elseif working_segment > 999999999999999 then
|
|
quantity = 1
|
|
count = prefix .. string.format("%015d", working_segment - 1000000000000000) .. suffix
|
|
offset = offset - 15
|
|
-- If it's not the most-significant segment, we need to pad the segment
|
|
-- out with zeros.
|
|
else
|
|
count = prefix .. string.format("%015d", working_segment) .. suffix
|
|
continue = false
|
|
end
|
|
end
|
|
end
|
|
-- Finally, we save the incremented value.
|
|
storage:set_string(key, count)
|
|
end
|
|
-- If unlimited counting is disabled though, we just add the quantity
|
|
-- to the current value, without doing anything special. The Minetest
|
|
-- mod storage API will automatically overflow for us at the 32-bit
|
|
-- signed integer limit. Realistically, no one will hit this limit, so
|
|
-- this is just fine for normal gameplay.
|
|
else
|
|
function increment(key, quantity)
|
|
storage:set_int(key, storage:get_int(key) + quantity)
|
|
end
|
|
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_scaled_player_level(player:get_player_name())
|
|
for modname, funct in next, levelup_functions do
|
|
funct(player, level)
|
|
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 liblevelup_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
|
|
-- Is this drop even counted?
|
|
if is_counted(original_drops.node_dug, original_drops) then
|
|
update_stats = true
|
|
local player_name = digger:get_player_name()
|
|
for _, item in next, original_drops do
|
|
local stack = ItemStack(item)
|
|
local count = stack:get_count()
|
|
stack:set_count(1)
|
|
local material_name = stack:to_string()
|
|
increment(player_name..";"..material_name, count)
|
|
-- We need to check the player's level now, before updating it, so we
|
|
-- can compare it to the new value.
|
|
local previous_level = get_scaled_player_level(player_name)
|
|
-- Now we can recalculate the player's level.
|
|
unscaled_level_cache[player_name][material_name] = calculate_unscaled_partial_level(player_name, material_name)
|
|
calculate_scaled_partial_levels(player_name)
|
|
-- With both the new and old level available, we can check to see if
|
|
-- there was an increase.
|
|
if previous_level ~= get_scaled_player_level(player_name, material_name) then
|
|
update_level = true
|
|
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
|
|
-- too.
|
|
if update_level then
|
|
call_levelup_functions(digger)
|
|
end
|
|
end
|
|
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
|
|
|
|
-- Sometimes, a mod may need code to run when the game begins, but
|
|
-- after liblevelup 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
|
|
-- liblevelup 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()
|
|
-- We'll need to iterate over a list of the drops later on so we can
|
|
-- build the sorted_drops table, so we should build that list as we go.
|
|
-- Once this function completes though, the list will no longer be
|
|
-- needed in this form and can be discarded. From there, we'll only
|
|
-- need the sorted_drops table to get this information if we need to
|
|
-- later iterate over it again.
|
|
local registered_drops = {}
|
|
-- 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 blacklisted = {}
|
|
-- 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
|
|
blacklisted[drop_key(recipe.items)] = true
|
|
end
|
|
end
|
|
for _, possibility in next, possibilities do
|
|
local key = drop_key(possibility)
|
|
if not blacklisted[key] and form.string ~= key and key ~= ""
|
|
and is_countable(form.string, possibility, possibilities, key) then
|
|
if not registered_countables[form.string] then
|
|
registered_countables[form.string] = {}
|
|
end
|
|
registered_countables[form.string][key] = true
|
|
for _, item in next, possibility do
|
|
local stack = ItemStack(item)
|
|
if not stack:is_empty() then
|
|
stack:set_count(1)
|
|
registered_drops[stack:to_string()] = stack:get_name()
|
|
end
|
|
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 sorted_drops drops table, of which a copy of is
|
|
-- given to any mod that requests it. While we're building this table,
|
|
-- 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. We also build
|
|
-- the drop_max_stack_size table here, used in the levelling equation
|
|
-- of each drop stat.
|
|
local denominator = math.log(max_stack_size)
|
|
for drop_string, drop_name in next, registered_drops do
|
|
sorted_drops[#sorted_drops+1] = drop_string
|
|
if minetest.registered_items[drop_name] then
|
|
drop_max_stack_size[drop_string] = minetest.registered_items[drop_name].stack_max
|
|
else
|
|
drop_max_stack_size[drop_string] = minetest.registered_items.unknown.stack_max
|
|
end
|
|
local numerator = math.log(drop_max_stack_size[drop_string])
|
|
levelling_exponent[drop_string] = numerator/denominator
|
|
reverse_levelling_exponent[drop_string] = denominator/numerator
|
|
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 = liblevelup_handle_node_drops
|
|
-- It's too late now to register startup functions. These should be
|
|
-- registered at load time, not run time, as it'd be too late for them
|
|
-- to get called if registered later. The same applies to the
|
|
-- is_countable functions. However, it's not even just these two
|
|
-- registrations that don't work after the game starts. Due to the way
|
|
-- registration is implemented, no registration will be successful once
|
|
-- the game starts. We might as well remove the whole registration
|
|
-- function table, as none of it is useful at this point in execution.
|
|
liblevelup.register = nil
|
|
-- We should remove the unregistration table as well.
|
|
liblevelup.unregister = nil
|
|
-- We're now completely done finding drop possibilities to count for
|
|
-- stats. We can delete the registered handlers while we're at it, as
|
|
-- we're done with them.
|
|
is_countable_callbacks = nil
|
|
-- The liblevelup 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)
|
|
|
|
-- To lock a craft, we simply return an empty ItemStack() if the
|
|
-- unlocking requirements have not been met after reporting to the
|
|
-- player what the unmet requirements are.
|
|
minetest.register_craft_predict(function(itemstack, player, old_craft_grid, craft_inv)
|
|
local lock = craft_locks[itemstack:get_name()]
|
|
if lock then
|
|
local disallowed = false
|
|
local name = player:get_player_name()
|
|
for drop, level in next, lock do
|
|
if get_scaled_per_material_player_level(name, drop) < level then
|
|
disallowed = true
|
|
minetest.chat_send_player(name, S("@1 level @2 is required for this craft.", ItemStack(drop):get_description(), level))
|
|
end
|
|
end
|
|
if disallowed then
|
|
return ItemStack("")
|
|
end
|
|
end
|
|
end)
|
|
|
|
-- Make the API visible to other mods:
|
|
liblevelup = {
|
|
|
|
-- Both for this mod's main purpose and for other purposes, it can be
|
|
-- useful to register callbacks that will be used when certain events
|
|
-- occur. If you register multiple callbacks of the same type from the
|
|
-- same mod, only the last one registered will be called by liblevelup.
|
|
-- If you need multiple things to happen, put them all in one function
|
|
-- or register a callback that calls all the functions you need called.
|
|
register = {
|
|
is_countable = register_is_countable ,
|
|
levelup_function = register_levelup_function,
|
|
startup_function = register_startup_function,
|
|
update_function = register_update_function ,
|
|
craft_lock = register_craft_lock ,
|
|
},
|
|
|
|
-- Sometimes though, you want to override the functionality of a mod,
|
|
-- so you don't want the old mod's callbacks called. These functions
|
|
-- let you unregister most callbacks given by other mods. The argument
|
|
-- to pass to these functions is always the name of the mod you wish to
|
|
-- unregister a callback provided by, and it doesn't prevent that mod
|
|
-- from later registering a callback. What that means is that you'll
|
|
-- want to depend on (either a hard dependency or a soft dependency)
|
|
-- the mod you want to restrict from having a registered callback, so
|
|
-- you can wait for it to register the callback and you can
|
|
-- successfully unregister that callback.
|
|
unregister = {
|
|
is_countable = unregister_is_countable ,
|
|
levelup_function = unregister_levelup_function,
|
|
update_function = unregister_update_function ,
|
|
craft_lock = remove_craft_lock ,
|
|
},
|
|
|
|
-- These methods use data queried form the database, process it, and
|
|
-- return it to the caller. liblevelup.get.pair_stat() is the only one
|
|
-- that returns data directly form the database unprocessed. All other
|
|
-- functions in this sub-table are built up from this one.
|
|
--
|
|
-- For any method taking a player name, the player name argument comes
|
|
-- first. Node names are always specified before drop names, if
|
|
-- specified at all. Part of the API overhaul was to ensure not only
|
|
-- organisation, but also consistency, so all future functions will
|
|
-- follow this convention as well.
|
|
get = {
|
|
stat = get_drop_stat ,
|
|
next_level_at = next_level_at ,
|
|
player_level = get_scaled_player_level ,
|
|
player_material_level = get_scaled_per_material_player_level,
|
|
player_material_progress_bar_length = player_material_progress_bar_length ,
|
|
},
|
|
|
|
-- These methods return metadata about how liblevelup is interacting
|
|
-- with the current subgame.
|
|
meta = {
|
|
drops_list = get_all_drops,
|
|
is_counted = is_counted ,
|
|
},
|
|
|
|
}
|
|
-----------------------------------------------------------------------
|
|
-------------------------- Deprecated Stuff: --------------------------
|
|
-----------------------------------------------------------------------
|
|
|
|
-- Legacy support in this mod is only enabled when overall Minetest
|
|
-- legacy support is enabled in minetest.conf. By default, legacy
|
|
-- support is enabled for release versions but not development
|
|
-- versions.
|
|
if minetest.settings:get("deprecated_lua_api_handling") ~= "error" then
|
|
-- I deprecated the entire API. It was a bit of a mess, and the mod's
|
|
-- name wasn't very fitting of its purpose either, so I renamed the mod
|
|
-- and rebuilt the API from the ground up with consistency and
|
|
-- organisation in mind. This is the old API table, kept for backwards
|
|
-- compatibility for at least two years, as is the usual time frame for
|
|
-- keeping deprecated code around in this mod.
|
|
--
|
|
-- Deprecated on 2020-02-29; DO NOT REMOVE FOR AT LEAST TWO YEARS.
|
|
__minestats__ = {
|
|
get_all_drops = get_all_drops ,
|
|
get_all_pairs = function()
|
|
return {}
|
|
end,
|
|
get_drop_stat = get_drop_stat ,
|
|
get_pair_stat = get_drop_stat ,
|
|
get_total_level = get_scaled_player_level ,
|
|
is_counted = is_counted ,
|
|
next_level_at = get_scaled_player_level ,
|
|
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 ,
|
|
get_proficiency_level = function(player_name, material)
|
|
return math.floor(get_unscaled_partial_level(player_name, material))
|
|
end,
|
|
get_drop_limit_multiplier = function()
|
|
return 1
|
|
end,
|
|
get_engine_stacks_mined = function(player_name, drop_name)
|
|
if minetest.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,
|
|
get_stacks_mined = function(player_name, drop_name)
|
|
if drop_max_stack_size[drop_name] then
|
|
return math.floor(get_stacks_mined(player_name, drop_name))
|
|
else
|
|
return 0
|
|
end
|
|
end,
|
|
}
|
|
|
|
-- Unscaled levels are no longer queriable by other mods, as scaled
|
|
-- levels should be used instead in every case so that players who
|
|
-- engage in a variety of activities are rewarded more than players
|
|
-- that learn to perform one activity endlessly but quickly.
|
|
--
|
|
-- Also, full player levels are now only earned by earning a level in
|
|
-- a specific material. That is, you can't earn half a level in one
|
|
-- material and half in another to earn a full player level. This
|
|
-- feature was removed to make player-visible level displays more
|
|
-- intuitive to players, but also provides an easy way to deal with the
|
|
-- fact that progress bars using partial levels along with the new
|
|
-- scaled level system require complex mathematics that I was never
|
|
-- able to figure out.
|
|
--
|
|
-- Deprecated on 2020-12-16; DO NOT REMOVE FOR AT LEAST TWO YEARS.
|
|
liblevelup.get.scaled_player_level = get_scaled_player_level
|
|
liblevelup.get.next_material_level_at = next_level_at
|
|
|
|
-- Node data is no longer recorded, as it's not useful to the purpose
|
|
-- of liblevelup and merely inflates the database size.
|
|
--
|
|
-- Deprecated on 2021-09-30; DO NOT REMOVE FOR AT LEAST TWO YEARS.
|
|
liblevelup.get.pair_stat = liblevelup.get.stat
|
|
liblevelup.get.drop_stat = liblevelup.get.stat
|
|
function liblevelup.meta.pairs_list()
|
|
return {}
|
|
end
|
|
startup_functions["*DEPRECATED 2021-09-30*"] = function()
|
|
local updated = false
|
|
local new_fields = {}
|
|
local fields = storage:to_table().fields
|
|
for key, value in next, fields do
|
|
local parts = string.split(key, ";")
|
|
if #parts == 2 then
|
|
new_fields[key] = (new_fields[key] or 0) + value
|
|
else
|
|
local stack = ItemStack(parts[#parts])
|
|
local count = stack:get_count()
|
|
stack:set_count(1)
|
|
local new_key = parts[1]..";"..stack:to_string()
|
|
new_fields[new_key] = (new_fields[new_key] or 0) + value * count
|
|
updated = true
|
|
end
|
|
end
|
|
if updated then
|
|
storage:from_table({
|
|
fields = new_fields,
|
|
})
|
|
end
|
|
end
|
|
|
|
end
|