12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403 |
- -- 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
|