--[[ Research N' Duplication Copyright (C) 2020 Noodlemire This library 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 library 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 library; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA --]] --file-specific global storage rnd.research = {} --Every researchable item has a specific amount needed to be researched in order to unlock duplication. --This table stores the research requirement of each, indexed by item name. rnd.research.goals = {} --Tracks research progress of each item of each player rnd.research.progs = {} -- Translation support local S = minetest.get_translator("rnd") local F = minetest.formspec_escape --This variable is needed to prevent buggy behavior when players grab an item from the sfinv research tab, --switch to a different tab, and place the item somewhere. --Without it, the game erroneously displays the /research menu in this scenario. local normal_research_menu_active = {} --In research.txt, research requirements of entire groups can be defined. --In case an item fits into numberous groups, the smallest research requirement takes precedent over larger research requirements. --Note that if a multi-group item is named specifically in research.txt or fits into group:rnd_goal, --that research requirement is used no matter how many group requirements are defined. --The point of returning rnd.research.goals[item] is to confirm that anything was found at all for an if statement. local function find_research_groups(item, research_specs) for k, v in pairs(research_specs) do if (not rnd.research.goals[item] or v < rnd.research.goals[item]) and k:sub(1, 6) == "group:" and minetest.get_item_group(item, k:sub(7)) > 0 then rnd.research.goals[item] = v end end return rnd.research.goals[item] end --Once all mods have been loaded, no more items can be registered, so this is when research requirements are calculated. minetest.register_on_mods_loaded(function() --Open research.txt in read-only mode local research_txt = io.open(rnd.mp.."research.txt", "r") --A list of each line in research.txt, indexed by item or group name and storing the associated research requirement of each local research_specs = {} --For each line in research.txt... for line in research_txt:lines() do --If the line isn't blank and isn't a comment as marked by a # at the start... if line:len() > 0 and line:sub(1, 1) ~= '#' then --Separate each line by the space in between the item/group name and research requirement number local line_parts = string.split(line, ' ') research_specs[line_parts[1]] = tonumber(line_parts[2]) end end --For each registered item... for item, def in pairs(minetest.registered_items) do --Skip over items marked as "not_in_creative_inventory" or "rnd_disabled", they aren't allowed to be researched. if minetest.get_item_group(item, "not_in_creative_inventory") == 0 and minetest.get_item_group(item, "rnd_disabled") == 0 then --If an item is named specifically in research.txt, that given requirement gets top priority. if research_specs[item] then rnd.research.goals[item] = research_specs[item] --If a mod creator gives their item the "rnd_goal" group, it has priority over group-wide goals defined in research.txt. elseif minetest.get_item_group(item, "rnd_goal") ~= 0 then rnd.research.goals[item] = minetest.get_item_group(item, "rnd_goal") --See the function above this one. elseif find_research_groups(item, research_specs) then --Nothing happens here because everything already happened in the function. This is only here so the default doesn't apply. else --By default, a full stack of items is needed to unlock duplication. rnd.research.goals[item] = def.stack_max end end end --Once finished, the file can be closed. io.close() end) --Whenever a player joins, progress data is either loaded from memory or created. minetest.register_on_joinplayer(function(player) local pname = player:get_player_name() --If the current game/world doesn't use the sfinv mod, commands are used to view menus. --This might not be intuitive, so players are reminded each time they join. --When sfinv is used, this isn't necessary, as those menus are integrated to the survival inventory. if not sfinv then --Research mode is redundant if the player already has creative mode. --This also saves on a bit of memory, since a progs table won't be created for creative players. if minetest.settings:get_bool("creative_mode") or (creative and creative.is_enabled_for(pname)) then minetest.chat_send_player(pname, S("Warning: You are in creative mode. Research and Duplication is disabled in favor of the default creative inventory.")) return else minetest.chat_send_player(pname, S)("Research and Duplication is active. You can use /research and /duplicate to bring up their respective menus.") end end --Short for "base", this is used to sort between saved research progress amounts. local b = pname.."_" --Checks if data has been created yet. local hasData = rnd.storage.getBool(b.."hasData") --Create a new research progress table for the newly joined player. rnd.research.progs[pname] = {} if hasData then --If this is a returning player, fill the new table with saved data. --Do this by checking the name of every researchable item, and seeing if data for "_" exists. for item, _ in pairs(minetest.registered_items) do if minetest.get_item_group(item, "not_in_creative_inventory") == 0 and minetest.get_item_group(item, "rnd_disabled") == 0 then local itemdata = rnd.storage.getNum(b..item) if itemdata then rnd.research.progs[pname][item] = itemdata end end end else rnd.storage.put(b.."hasData", true) --Create a research inventory for that player. player:get_inventory():set_size("research", 8) --For sorting purposes, creation of the duplication inventory is moved to duplication.lua. rnd.duplication.init(player) end end) --When a player leaves, their progress data doesn't need to be loaded in progs[] anymore, since it's in mod storage. minetest.register_on_leaveplayer(function(player) rnd.research.progs[player:get_player_name()] = nil end) --Use this to check if a player's research of any particular item is complete. function rnd.research.complete(pname, iname) --If the given item can't be researched, automatically say no. if not rnd.research.progs[pname] or not rnd.research.progs[pname][iname] or not rnd.research.goals[iname] then return false end --Otherwise, research is complete has long as progress has reached or exceeded the goal. --(Excess is normally prevented, but if a file is modified and a goal becomes lowered, it can still happen.) return rnd.research.progs[pname][iname] >= rnd.research.goals[iname] end --This function is used to determine how much progress to add, based on the returned number. function rnd.research.progress(pname, iname, num) --If the item can't be researched, always return 0. if not rnd.research.progs[pname] or not rnd.research.goals[iname] or rnd.research.complete(pname, iname) then return 0 end --Give whichever is smaller: the provided amount of items to use, or the amount needed to complete research. return math.min(num, rnd.research.goals[iname] - (rnd.research.progs[pname][iname] or 0)) end --This function creates the research menu. local function research_formspec(player, unified) --Get the player's inventory to look through later. local inv = player:get_inventory() --Stored information to displayer later in the menu. --"X" is the default goal to show that an item can't be researched. local item = {name = "", count = 0, goal = "X"} --For each item currently in the research inventory, search for its name. --Only one kind of item is allowed in the research inventory at a time, so when one is found, the loop can be exited immediately. for i = 1, inv:get_size("research") do local stack = inv:get_stack("research", i) if not stack:is_empty() then item.name = stack:get_name() break end end --If a name was found, get information about current progress and its goal. if item.name ~= "" then item.count = rnd.research.progs[player:get_player_name()][item.name] or item.count item.goal = rnd.research.goals[item.name] or item.goal end --The progress label's text is stored here because it is used more than once. local progString = "("..item.count.."/"..item.goal..")" --Show a different inventory layout based on if the unified_inventory is open or not local base_inv = rnd.base_inv_formspec if unified then base_inv = rnd.base_unified_formspec end --With all of the above information, the formspec can be formed here. --Note that every time information changes, the menu is reformed. return base_inv.. "label["..tostring(4 - 0.05 * item.name:len())..",0.75;"..item.name.."]".. "label["..tostring(3.9 - 0.05 * progString:len())..",1;"..progString.."]".. "button[3,1.5;2,1;research;"..F(S("Research")).."]".. "list[current_player;research;0,2.5;8,1;]".. "listring[]" end --This function forces the research menu to only accept once item at a time. --The extra slots are only here to make it possible to research several stacks at a time, for items with high requirements. minetest.register_allow_player_inventory_action(function(player, action, inventory, inventory_info) --Putting items directly into the research menu from nowhere isn't normally possible, so just block it. if action == "put" and inventory_info.listname == "research" then return 0 --If the player tries to move an item from an inventory to the research inventory... elseif action == "move" and inventory_info.to_list == "research" then --This overly long set of functions and variables is only to get the name of the item being moved. local iname = inventory:get_stack(inventory_info.from_list, inventory_info.from_index):get_name() --For each item in the research menu... for i = 1, inventory:get_size(inventory_info.to_list) do local otherItem = inventory:get_stack(inventory_info.to_list, i) --Only keep going if the current slot is where the item is being moved, if the current slot is empty, --or the item that is in this slot has the same name as the item being moved to the research inventory. --Otherwise, prevent the movement. if i ~= inventory_info.to_index and not otherItem:is_empty() and iname ~= otherItem:get_name() then return 0 end end --If we got this far, we can let the itemstack into the research inventory. end end) --Whenever something is successfully moved to the research menu, reform it to adjust the research progress labels. minetest.register_on_player_inventory_action(function(player, action, inventory, inventory_info) if action == "move" and (inventory_info.to_list == "research" or inventory_info.from_list == "research") then local pname = player:get_player_name() --A different method must be used if the sfinv inventory is open, so the tabs still work after the reformation. if (unified_inventory and not normal_research_menu_active[pname]) then unified_inventory.set_inventory_formspec(player, "research") elseif sfinv and sfinv.get_or_create_context(player).page == "research" then sfinv.set_player_inventory_formspec(player) --Check first that the /research menu is open. If it is, update it. elseif normal_research_menu_active[pname] then minetest.show_formspec(pname, "research", research_formspec(player)) end end end) --The functionality between the research menu's Research button. --This function is defined like this because it is used in two different places, one for sfinv and one that is used for /research. local function on_player_receive_fields_research(player, formname, fields, context) local pname = player:get_player_name() if fields["research"] and ((unified_inventory and formname == "") or (formname == "research")) then --Get the player's inventory to look through later. local inv = player:get_inventory() --Information stored about the item, for later research local item = {name = "", count = 0} --Look through each slot in the research inventory, to fill out the item table. for i = 1, inv:get_size("research") do local stack = inv:get_stack("research", i) if not stack:is_empty() then item.count = item.count + stack:get_count() item.name = stack:get_name() end end --If items were found... if item.name ~= "" then --Calculate the amount of progress to add. local progress = rnd.research.progress(pname, item.name, item.count) --Add the progress to the progs table and immediately save it in mod storage. rnd.research.progs[pname][item.name] = (rnd.research.progs[pname][item.name] or 0) + progress rnd.storage.put(pname.."_"..item.name, rnd.research.progs[pname][item.name]) --Reform the research inventory, using a method to preserve sfinv tabs if they exist. --Its placement here, before the research inventory is cleared, is important. --This makes the research label "lag", so if all of the items are removed by research, --The player will still see the total research progress for that item, rather than the usual (0/X) --However, unified_inventory is too aggressive for this trick to work. Sorry. if unified_inventory and formname == "" then unified_inventory.set_inventory_formspec(player, "research") elseif sfinv and context then sfinv.set_player_inventory_formspec(player) else minetest.show_formspec(pname, "research", research_formspec(player)) end --For each research inventory slot... --Note that from here on out, the progress variable is only used to determine how many items need to be removed. for i = 1, inv:get_size("research") do local stack = inv:get_stack("research", i) --If the stack is larger than the amount of progress made, deplete that amount from the stack and leave everything else be. if stack:get_count() > progress then stack:set_count(stack:get_count() - progress) inv:set_stack("research", i, stack) break else --Otherwise, decrase progress for future reference in this loop and erase the stack progress = progress - stack:get_count() inv:set_stack("research", i, {}) --If progress is 0, because the remaining progress equalled this stack's size, this loop can be exited. if progress == 0 then break end end end end end if normal_research_menu_active[pname] and fields["quit"] then --If the player just exited the /research menu, remember that it is no longer open. normal_research_menu_active[pname] = false end end --This applies the above function to /research. minetest.register_on_player_receive_fields(on_player_receive_fields_research) --For use when sfinv isn't active. minetest.register_chatcommand("research", { params = "", description = S("Open the research menu."), privs = {}, func = function(name, param) --Block creative players from using it. if minetest.settings:get_bool("creative_mode") or (creative and creative.is_enabled_for(name)) then minetest.chat_send_player(name, S("Creative Mode players can't use this.")) else --Remember that the /research menu is being used right now. normal_research_menu_active[name] = true minetest.show_formspec(name, "research", research_formspec(minetest.get_player_by_name(name))) end end, }) --When unified_inventory is active, the research tab is defined here, using several previously defined functions. if unified_inventory then unified_inventory.register_page("research", { get_formspec = function(player) return { formspec = research_formspec(player, true), draw_inventory = false, } end }) unified_inventory.register_button("research", { type = "image", image = "rnd_button_research_page.png", tooltip = S("Research"), condition = function(player) return not unified_inventory.is_creative(player) end }) --When sfinv is active, the research tab is defined here, using several previously defined functions. elseif sfinv then sfinv.register_page("research", { title = S("Research"), get = function(self, player, context) return sfinv.make_formspec(player, context, research_formspec(player)) end, is_in_nav = function(self, player, context) return not (minetest.settings:get_bool("creative_mode") or (creative and creative.is_enabled_for(player))) end, on_player_receive_fields = function(self, player, context, fields) on_player_receive_fields_research(player, context.page, fields, context) --When this tab is exited, the player will see the home tab upon seeing the inventory next. --This prevents /research from being bugged if it's used after closing the research tab. if fields["quit"] then sfinv.set_page(player, sfinv.get_homepage_name(player)) end end, }) end