409 lines
17 KiB
Lua
409 lines
17 KiB
Lua
--[[
|
|
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 "<player name>_<item name>" 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
|