diff --git a/README.md b/README.md index d0f552b..5ec75bf 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,24 @@ -# rnd -Inspired directly by Terraria's 1.4 update, this mod offers an "earned creative" mode to survival players. When active, you can use a specific amount of an item for research. Once an item is fully researched, it is permanently unlocked in the duplication menu, letting you create an infinite amount of that item. +------------------------------------------------------------------------------------------------------------- +Research N' Duplication +[rnd] +------------------------------------------------------------------------------------------------------------- + +------------------------------------------------------------------------------------------------------------- +About +------------------------------------------------------------------------------------------------------------- +This mod is inspired directly by the main feature of Terraria's "Journey Mode". It is a pseudo-creative mode where it is possible to create infinite resources, but you first have to earn that ability per item by obtaining a specific quantity of said item, and then using it for "research". This means that instead of having god-like powers from the start, you have to play the game in a survival-esque way, using mostly traditional methods, with duplication often only aiding in reducing the total amount of grinding, and in some cases, providing more of a reason to build big, intricate, and/or luxurious structures. + +------------------------------------------------------------------------------------------------------------- +Dependencies and Support +------------------------------------------------------------------------------------------------------------- +No hard dependencies are necessary. There is support for sfinv, though, which lets the research and duplication menus appear as tabs in the regular survival inventory. Without it, those menus are accessed through the /research and /duplicate commands. + +------------------------------------------------------------------------------------------------------------- +License +------------------------------------------------------------------------------------------------------------- +The LGPL v2.1 License is used with this mod. See https://www.gnu.org/licenses/old-licenses/lgpl-2.1.en.html or LICENSE.txt for more details. + +------------------------------------------------------------------------------------------------------------- +Installation +------------------------------------------------------------------------------------------------------------- +Download, unzip, and place within the usual minetest/current/mods folder, and it will behave in relation to the Minetest engine like any other mod. diff --git a/duplication.lua b/duplication.lua new file mode 100644 index 0000000..bdede0c --- /dev/null +++ b/duplication.lua @@ -0,0 +1,237 @@ +--[[ +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.duplication = {} + +--Stores each player's current selected duplication page, which is important only so that page selection works at all. +rnd.duplication.currentPage = {} + + + +--Create both the player's duplication inventory, and a hidden trash inventory so that items in the survival inventory can be shift-click-dumped. +function rnd.duplication.init(player) + player:get_inventory():set_size("duplication", 32) + player:get_inventory():set_size("trash", 1) + minetest.log("init trash") +end + + + +--Count the amount of items that the player has completely researched, to determine how many pages there are. +local function countComplete(pname) + local count = 0 + + for iname, iprog in pairs(rnd.research.progs[pname]) do + if rnd.research.complete(pname, iname) then + count = count + 1 + end + end + + return count +end + +--A version of the pairs() function that sorts keys alphabetically. +local function spairs(t) + --Collect the keys + local keys = {} + + for k in pairs(t) do + keys[#keys + 1] = k + end + + --Sort them + table.sort(keys) + + --Return the iterator function + local i = 0 + + return function() + i = i + 1 + + if keys[i] then + return keys[i], t[keys[i]] + end + end +end + +--This function creates the duplication menu. +local function duplication_formspec(player, page) + local pname = player:get_player_name() + + --Get the player's inventory to look through later. + local inv = player:get_inventory() + + --Show either the provided page number, or default to the first page. + rnd.duplication.currentPage[pname] = page or 1 + + --An iterator for the upcoming loop + local i = 1 + --The start point, to determine when to start adding items to the duplication inventory. + local itemI = (rnd.duplication.currentPage[pname] - 1) * 32 + 1 + + --Clear the duplication inventory beforehand. + inv:set_list("duplication", {}) + + --For each item that this player has researched so far... + for iname, iprog in spairs(rnd.research.progs[pname]) do + --If research for this item is complete... + if rnd.research.complete(pname, iname) then + --If the iterator has reached or passed the starting point... + if i >= itemI then + --Create a new itemstack based on the current item's name + local stack = ItemStack({name = iname}) + --Set its count to this item's specific maximum stack size + stack:set_count(stack:get_stack_max()) + + --Finally, add it to the inventory at the appropriate slot. + inv:set_stack("duplication", (1 + i - itemI), stack) + end + + --Iterate now + i = i + 1 + + --If the iterator has made enough progress to fill the entire page, this loop can be exited. + if i > itemI + 32 then + break + end + end + end + + ----The page number label's text is stored here because it is used more than once. + local pageString = tostring(rnd.duplication.currentPage[pname]).."/"..tostring(math.ceil(countComplete(pname) / 32)) + + --With all of the above information, the formspec can be formed here. + --Note that every time information changes, the menu is reformed. + --Also, note the extra listring to trash, which enables deletion of items in the survival inventory via shift-clicking. + return rnd.base_inv_formspec.. + "button[1.5,4;1,1;frst;<<]".. + "button[2.5,4;1,1;prev;<]".. + "button[4.5,4;1,1;next;>]".. + "button[5.5,4;1,1;last;>>]".. + "label[3.7,4;Page]".. + "label["..tostring(3.9 - 0.05 * pageString:len())..",4.25;"..pageString.."]".. + "list[current_player;duplication;0,0;8,4;]".. + "listring[current_player;duplication]".. + "listring[current_player;main]".. + "listring[current_player;trash]" +end + +--This function prevents players from placing anything inside the duplication inventory. +function rnd.duplication.allow_player_inventory_action(player, action, inventory, inventory_info) + if (action == "put" and inventory_info.listname == "duplication") or (action == "move" and inventory_info.to_list == "duplication") then + return 0 + end + + return inventory_info.count or inventory_info.stack:get_count() +end + +--This function refills the duplication inventory whenever something is removed, and empties the trash slot whenever it is filled. +minetest.register_on_player_inventory_action(function(player, action, inventory, inventory_info) + --Slightly different means are necessary when items are put in the survival inventory or thrown on the ground + --due to differences in inventory_info variables when the action is "take" or "move". + if action == "take" and inventory_info.listname == "duplication" then + inventory:set_stack("duplication", inventory_info.index, inventory_info.stack) + elseif action == "move" and inventory_info.from_list == "duplication" then + local stack = inventory:get_stack(inventory_info.to_list, inventory_info.to_index) + stack:set_count(stack:get_stack_max()) + + inventory:set_stack("duplication", inventory_info.from_index, stack) + end + + if action == "move" and inventory_info.to_list == "trash" then + inventory:set_list("trash", {}) + end +end) + +--Defines functionality for the page change buttons. +--This function is defined like this because it is used in two different places, one for sfinv and one that is used for /duplicate. +local function on_player_receive_fields_duplication(player, formname, fields, context) + --If one of the four buttons were pressed... + if formname == "duplication" and (fields["frst"] or fields["prev"] or fields["next"] or fields["last"]) then + local pname = player:get_player_name() + local page = rnd.duplication.currentPage[pname] + + --The first page is always page 1 + if fields["frst"] then + page = 1 + --Decrease the page number. If it dips below 1, keep it at 1. + elseif fields["prev"] then + page = math.max(1, page - 1) + --Increase the page number. If it exceeds the last page, set it back to the last page. + elseif fields["next"] then + page = math.min(math.ceil(countComplete(pname) / 32), page + 1) + --Get the amount of pages to find the last page. + elseif fields["last"] then + page = math.ceil(countComplete(pname) / 32) + end + + --When a page changes, the duplication menu has to be reformed. + --A different method must be used if the sfinv inventory is open, so the tabs still work after the reformation. + if sfinv and context then + rnd.duplication.currentPage[pname] = page + sfinv.set_player_inventory_formspec(player) + else + minetest.show_formspec(pname, "duplication", duplication_formspec(player, page)) + end + end +end +--This applies the above function to /duplicate. +minetest.register_on_player_receive_fields(on_player_receive_fields_duplication) + +--For use when sfinv isn't active. +minetest.register_chatcommand("duplicate", { + params = "", + description = "Open the duplication 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, "Creative Mode players can't use this.") + else + minetest.show_formspec(name, "duplication", duplication_formspec(minetest.get_player_by_name(name))) + end + end, +}) + +--When sfinv is active, the duplication tab is defined here, using several previously defined functions. +if sfinv then + sfinv.register_page("duplication", { + title = "Duplicate", + + get = function(self, player, context) + return sfinv.make_formspec(player, context, duplication_formspec(player, rnd.duplication.currentPage[player:get_player_name()])) + 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_duplication(player, context.page, fields, context) + + --When this tab is exited, the player will see the home tab upon seeing the inventory next. + --This prevents /duplicate 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 diff --git a/init.lua b/init.lua new file mode 100644 index 0000000..a79aaaf --- /dev/null +++ b/init.lua @@ -0,0 +1,50 @@ +--[[ +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 +--]] + +--Mod-specific global variable +rnd = {} + +--MP = Mod Path +rnd.mp = minetest.get_modpath(minetest.get_current_modname())..'/' + + + +--If default is loaded, grab its hotbar background image +local hotbar_bg = "" + +if default then + hotbar_bg = default.get_hotbar_bg(0,5.2) +end + +--The size and player inventory menu, used for both the research and duplication menus +rnd.base_inv_formspec = "size[8,9.1]".. + "list[current_player;main;0,5.2;8,1;]".. + "list[current_player;main;0,6.35;8,3;8]".. + hotbar_bg + + + +--A custom API file that I find a little more convenient than the usual mod storage API. +dofile(rnd.mp.."storage.lua") + +--All of the functionality of the research menu +dofile(rnd.mp.."research.lua") + +--All of the functionality of the duplication menu +dofile(rnd.mp.."duplication.lua") diff --git a/mod.conf b/mod.conf new file mode 100644 index 0000000..df4e000 --- /dev/null +++ b/mod.conf @@ -0,0 +1,3 @@ +name = rnd +description = Research N' Duplicate: Obtain and research enough of a specific item to unlock the ability to infinitely duplicate it. In other words, it's an "earned" creative mode. +optional_depends = creative, default, sfinv diff --git a/research.lua b/research.lua new file mode 100644 index 0000000..bc85908 --- /dev/null +++ b/research.lua @@ -0,0 +1,376 @@ +--[[ +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 = {} + +--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, "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, "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) + --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..")" + + --With all of the above information, the formspec can be formed here. + --Note that every time information changes, the menu is reformed. + return rnd.base_inv_formspec.. + "label["..tostring(4 - 0.05 * item.name:len())..",1.75;"..item.name.."]".. + "label["..tostring(3.9 - 0.05 * progString:len())..",2;"..progString.."]".. + "button[3,2.5;2,1;research;Research]".. + "list[current_player;research;0,3.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. + return inventory_info.count + end + + --For sorting purposes, the next part of the function is in duplication.lua. + return rnd.duplication.allow_player_inventory_action(player, action, inventory, inventory_info) +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 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 formname == "research" and fields["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) + if 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 = "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, "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 sfinv is active, the research tab is defined here, using several previously defined functions. +if sfinv then + sfinv.register_page("research", { + title = "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 diff --git a/research.txt b/research.txt new file mode 100644 index 0000000..d4c0c9a --- /dev/null +++ b/research.txt @@ -0,0 +1,87 @@ +#Use this file to specify research requirements for items, given specific names or groups. +#An item name will always take precedence over a group name. +#Group names may not always apply as you'd expect, if a modder added that item to the "rnd_goal" group in their code. +#Specific item names will override rnd_goal. + +#Give research requirements to items by first writing that item's internal name, followed by a space and a number. +#For groups, write "group:" followed by the group's internal name. Then follow that with a space and a number. +#The number you write is the exact amount of items needed to research in order to unlock duplication. +#It's okay to write the names of items from any other mods, even mods that aren't loaded. + +#It's important to note that if no goal is defined at all, items default to their given stack size. +#For most items, this means the research requirement is 99. For tools, however, the requirement is 1. + + + +#Some items, like slabs and stairs, have requirements based on how many you can get per block. +#This is especially so for slabs, which can be made back into solid blocks easily. +group:slab 198 +group:stair 149 + +#A number of crafted items have a goal of 1 for the sake of convenience, since you only make one at a time. +#Plus, if you're making these, you'll almost always have the materials researched already. +beds:bed 1 +beds:fancy_bed 1 +boats:boat 1 +carts:cart 1 +default:furnace 1 +doors:door_wood 1 +doors:door_steel 1 +doors:door_glass 1 +doors:door_obsidian_glass 1 +doors:trapdoor 1 +doors:trapdoor_steel 1 +doors:gate_wood 1 +doors:gate_acacia_wood 1 +doors:gate_junglewood 1 +doors:gate_pine_wood 1 +doors:gate_aspen_wood 1 +xpanes:door_steel_bar 1 + +#Empty buckets are the only buckets that are allowed to stack, which I find weird. +bucket:bucket_empty 1 + +#Some research requirements are based on how frequently you get items. +#Although there are massive differences between different ore rarities, obtaining them is still the same process, +#via digging through through stone or wandering caves and hoping for the best. +#Therefore, these require less research than very readily available materials, with rarer materials being more difficult to research due to their power. +default:stone_with_coal 27 +default:coal_lump 27 +default:stone_with_copper 27 +default:copper_lump 27 +default:copper_ingot 27 +default:stone_with_tin 27 +default:tin_lump 27 +default:tin_ingot 27 +default:bronze_ingot 27 +default:stone_with_iron 27 +default:iron_lump 27 +default:steel_ingot 27 +default:stone_with_gold 27 +default:gold_lump 27 +default:gold_ingot 27 +default:stone_with_mese 27 +default:mese_crystal 27 +default:stone_with_diamond 27 +default:diamond 27 + +#Solid ore blocks are made from 9 of that ore, and can be made back into 9 ore each. So, slab logic applies here. +default:coalblock 3 +default:copperblock 3 +default:tinblock 3 +default:bronzeblock 3 +default:steelblock 3 +default:goldblock 3 +default:meseblock 3 +default:diamondblock 3 + +#In the same way, 9 pieces of ore equal one usuably-sized unit of ore. +default:mese_crystal_fragment 243 + +#More slab logic, even these crafting processes can't normally be reversed. +default:paper 33 +default:book 11 + +#TNT Sticks need 9 because that's how many you need to make a block of TNT. +tnt:tnt_stick 9 +tnt:tnt 1 diff --git a/storage.lua b/storage.lua new file mode 100644 index 0000000..eced216 --- /dev/null +++ b/storage.lua @@ -0,0 +1,67 @@ +--[[ +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.storage = {} + +--Grab rnd's mod-private storage +local store = minetest.get_mod_storage() + +--A set function that automatically takes any kind of storable value +function rnd.storage.put(key, val) + local t = type(val) + + if t == "string" then + store:set_string(key, val) + elseif t == "number" then + store:set_float(key, val) + elseif val == true then + store:set_int(key, 1) + elseif not val then + --Setting a value to "" will delete it. + store:set_string(key, "") + else + minetest.log("error", "Attempt to put val of type "..t.." into key "..key) + end +end + +--Normally, get_string returns "" when there's no value. I find nil easier to check. +function rnd.storage.getStr(key) + if not store:contains(key) then return nil end + + return store:get_string(key) +end + +--Normally, get_float returns 0 when there's no value. I find nil easier to check. +--Also, "Num" is easier to remember, because the type() for numeral values in Lua is "number", rather than "float" +function rnd.storage.getNum(key) + if not store:contains(key) then return nil end + + return store:get_float(key) +end + +--Essentially only exists because it's memorable, alongside the rest of the "getType" functions +function rnd.storage.getBool(key) + return store:contains(key) +end + +--A value that isn't given at all counts as "not val" in the if statements of put(), which is why this works. +function rnd.storage.del(key) + rnd.storage.put(key) +end