Module:Item/recipes: Difference between revisions
		
		
		
		
		
		Jump to navigation
		Jump to search
		
				
		
		
	
 (The max level for awakened support gems is 5, not 6.)  | 
				Mefisto1029 (talk | contribs)   (pass as string)  | 
				||
| (149 intermediate revisions by 6 users not shown) | |||
| Line 1: | Line 1: | ||
-------------------------------------------------------------------------------  | -------------------------------------------------------------------------------  | ||
--    | --    | ||
--   | -- Recipes for Module:Item  | ||
--    | --    | ||
-------------------------------------------------------------------------------  | -------------------------------------------------------------------------------  | ||
local m_util = require('Module:Util')  | |||
local m_cargo = require('Module:Cargo')  | local m_cargo = require('Module:Cargo')  | ||
local   | |||
local m_game = mw.loadData('Module:Game')  | |||
-- Lazy loading  | -- Lazy loading  | ||
local f_modifier_link -- require('Module:Modifier   | local f_modifier_link -- require('Module:Modifier link').modifier_link  | ||
local   | -- Should we use the sandbox version of our submodules?  | ||
local use_sandbox = m_util.misc.maybe_sandbox('Item')  | |||
-- The cfg table contains all localisable strings and configuration, to make it  | -- The cfg table contains all localisable strings and configuration, to make it  | ||
-- easier to port this module to another wiki.  | -- easier to port this module to another wiki.  | ||
local cfg = mw.loadData('Module:  | local cfg = use_sandbox and mw.loadData('Module:Item/config/sandbox') or mw.loadData('Module:Item/config')  | ||
local i18n = cfg.i18n.  | local i18n = cfg.i18n.recipes  | ||
-- ----------------------------------------------------------------------------  | -- ----------------------------------------------------------------------------  | ||
| Line 25: | Line 28: | ||
local h = {}  | local h = {}  | ||
-- Lazy loading for Module:Modifier   | -- Lazy loading for Module:Modifier link  | ||
function h.modifier_link(args)  | function h.modifier_link(args)  | ||
     if not f_modifier_link then  |      if not f_modifier_link then  | ||
         f_modifier_link = require('Module:Modifier   |          f_modifier_link = require('Module:Modifier link').main  | ||
     end  |      end  | ||
     return f_modifier_link(args)  |      return f_modifier_link(args)  | ||
| Line 46: | Line 49: | ||
     -- Optional:  |      -- Optional:  | ||
     --  negate: negates the check against the value, i.e. whether the value is not equal or not in the list/table.  |      --  negate: negates the check against the value, i.e. whether the value is not equal or not in the list/table.  | ||
     args = args or {}  | |||
     -- Inner type of function depending on whether to check a single value, a list of values or an associative list of values  |      -- Inner type of function depending on whether to check a single value, a list of values or an associative list of values  | ||
| Line 74: | Line 75: | ||
     -- Outer type of function depending on whether to check a single value or against a table  |      -- Outer type of function depending on whether to check a single value or against a table  | ||
     return function (tpl_args  |      return function (tpl_args)  | ||
         local tpl_value = tpl_args[args.arg]  |          local tpl_value = tpl_args[args.arg]  | ||
         local rtr  |          local rtr  | ||
| Line 96: | Line 97: | ||
end  | end  | ||
function h.conditions.factory.  | function h.conditions.factory.not_arg(args)  | ||
     return function (tpl_args  |     args = args or {}  | ||
         for _,   |     args.negate = true  | ||
             if   |     return h.conditions.factory.arg(args)  | ||
end  | |||
function h.conditions.factory.flag_is_set(args)  | |||
    return function (tpl_args)  | |||
        return tpl_args._flags[args.flag] == true  | |||
    end  | |||
end  | |||
function h.conditions.factory.acquisition_tag(args)  | |||
     return function (tpl_args)  | |||
        local negate = args.negate or false  | |||
         for _, tag in ipairs(tpl_args.acquisition_tags or {}) do  | |||
             if tag == args.tag then  | |||
                return not negate  | |||
            end  | |||
        end  | |||
        return negate  | |||
    end  | |||
end  | |||
function h.conditions.factory.drop_monsters(args)  | |||
    return function (tpl_args)  | |||
        for _, monster in ipairs(tpl_args.drop_monsters or {}) do  | |||
            if string.find(monster, args.monster, 1, true) then  | |||
                 return true  |                  return true  | ||
             end  |              end  | ||
| Line 107: | Line 132: | ||
end  | end  | ||
h.conditions.  | function h.conditions.factory.drop_rarity(args)  | ||
    return function (tpl_args)  | |||
        for _, rarity in ipairs(tpl_args.drop_rarities_ids or {}) do  | |||
            if rarity == args.rarity then  | |||
                return true  | |||
            end  | |||
        end  | |||
        return false  | |||
    end  | |||
end  | |||
function h.conditions.item_class_has_corrupted_implicits(tpl_args  | function h.conditions.factory.drop_level_not_greater_than(args)  | ||
    return function (tpl_args)  | |||
        if tpl_args.drop_level == nil then  | |||
            return true  | |||
        end  | |||
        return tpl_args.drop_level <= args.level  | |||
    end  | |||
end  | |||
function h.conditions.item_class_has_corrupted_implicits(tpl_args)  | |||
     local groups = {  |      local groups = {  | ||
         cfg.class_groups.weapons.keys,  |          cfg.class_groups.weapons.keys,  | ||
| Line 118: | Line 160: | ||
     }  |      }  | ||
     for _, g in ipairs(groups) do  |      for _, g in ipairs(groups) do  | ||
         if h.conditions.factory.arg{arg='class_id', values_assoc=g}(tpl_args  |          if h.conditions.factory.arg{arg='class_id', values_assoc=g}(tpl_args) then  | ||
             return true  |              return true  | ||
         end  |          end  | ||
| Line 125: | Line 167: | ||
end  | end  | ||
function h.conditions.item_class_has_influences(tpl_args  | function h.conditions.item_class_has_influences(tpl_args)  | ||
     local groups = {  |      local groups = {  | ||
         cfg.class_groups.weapons.keys,  |          cfg.class_groups.weapons.keys,  | ||
| Line 133: | Line 175: | ||
     }  |      }  | ||
     for _, g in ipairs(groups) do  |      for _, g in ipairs(groups) do  | ||
         if h.conditions.factory.arg{arg='class_id', values_assoc=g}(tpl_args,   |          if h.conditions.factory.arg{arg='class_id', values_assoc=g}(tpl_args) and h.conditions.factory.not_arg{arg='class_id', value='FishingRod'}(tpl_args) then  | ||
            return true  | |||
        end  | |||
    end  | |||
    return false  | |||
end  | |||
function h.conditions.item_class_has_synthesised_implicits(tpl_args)  | |||
    local groups = {  | |||
        cfg.class_groups.weapons.keys,  | |||
        cfg.class_groups.armor.keys,  | |||
        cfg.class_groups.jewellery.keys,  | |||
        {['Quiver'] = true, ['Jewel'] = true, ['AbyssJewel'] = true},  | |||
    }  | |||
    for _, g in ipairs(groups) do  | |||
        if h.conditions.factory.arg{arg='class_id', values_assoc=g}(tpl_args) and h.conditions.factory.not_arg{arg='class_id', value='FishingRod'}(tpl_args) then  | |||
            return true  | |||
        end  | |||
    end  | |||
    return false  | |||
end  | |||
function h.conditions.item_class_has_fractured_modifiers(tpl_args)  | |||
    local groups = {  | |||
        cfg.class_groups.weapons.keys,  | |||
        cfg.class_groups.armor.keys,  | |||
        cfg.class_groups.jewellery.keys,  | |||
        {['Quiver'] = true, ['Jewel'] = true, ['Map'] = true},  | |||
    }  | |||
    for _, g in ipairs(groups) do  | |||
        if h.conditions.factory.arg{arg='class_id', values_assoc=g}(tpl_args) and h.conditions.factory.not_arg{arg='class_id', value='FishingRod'}(tpl_args) then  | |||
             return true  |              return true  | ||
         end  |          end  | ||
| Line 146: | Line 218: | ||
local c = {}  | local c = {}  | ||
c.named_conditions = {  | |||
    is_normal = h.conditions.factory.arg{arg='rarity_id', value='normal'},  | |||
    is_unique = h.conditions.factory.arg{arg='rarity_id', value='unique'},  | |||
     is_corrupted = h.conditions.factory.arg{arg='  |      is_not_drop_restricted = h.conditions.factory.arg{arg='is_drop_restricted', value=false},  | ||
     is_not_corrupted = h.conditions.factory.arg{arg='is_corrupted', value=false},  | |||
    is_not_replica = h.conditions.factory.arg{arg='is_replica', value=false},  | |||
    drop_level_ngt_divcard_default_max_ilvl = h.conditions.factory.drop_level_not_greater_than{level=cfg.divination_card_exchange_default_max_ilvl},  | |||
    item_class_has_corrupted_implicits = h.conditions.item_class_has_corrupted_implicits,  | |||
    item_class_has_influences = h.conditions.item_class_has_influences,  | |||
    item_class_has_synthesised_implicits = h.conditions.item_class_has_synthesised_implicits,  | |||
    item_class_has_fractured_modifiers = h.conditions.item_class_has_fractured_modifiers,  | |||
}  | }  | ||
-- Order matters!  | -- Order matters!  | ||
-- Put most specific outcome at the top and the least specific at the bottom.  | -- Put most specific outcome at the top and the least specific at the bottom.  | ||
c.  | c.automatic_recipes = {  | ||
--[[  | --[[  | ||
     {  |      {  | ||
         conditions = {  | |||
             function (tpl_args) end,  | |||
             function (tpl_args  | |||
         },  |          },  | ||
         text = '',  |          text = '',  | ||
         parts = {  | |||
             {  |              {  | ||
                 name = '',  |                  name = '',  | ||
| Line 172: | Line 248: | ||
         },  |          },  | ||
     },  |      },  | ||
]]  | |||
}  | }  | ||
| Line 2,556: | Line 258: | ||
local p = {}  | local p = {}  | ||
function p.  | function p.process_recipes(tpl_args)  | ||
     local query_data = {  |      local query_data = {  | ||
         id = {},  |          id = {},  | ||
| Line 2,562: | Line 264: | ||
         page = {},  |          page = {},  | ||
     }  |      }  | ||
     local   |      local recipes = {}  | ||
     -- ------------------------------------------------------------------------  |      -- ------------------------------------------------------------------------  | ||
     -- Manual data  |      -- Manual data  | ||
     -- ------------------------------------------------------------------------  |      -- ------------------------------------------------------------------------  | ||
     local   |      local recipe_num = #recipes + 1  | ||
     local   |      local recipe  | ||
     repeat  |      repeat  | ||
         local prefix = string.format('  |          local prefix = string.format('recipe%s_', recipe_num)  | ||
         local   |          local part_num = 1  | ||
         local   |          local part  | ||
         recipe = {  | |||
             parts = {},  | |||
             text = m_util.cast.text(tpl_args[prefix .. '  |             result_amount = tonumber(tpl_args[prefix .. 'result_amount']) or 1,  | ||
             text = m_util.cast.text(tpl_args[prefix .. 'description']),  | |||
             automatic = false,  |              automatic = false,  | ||
         }  |          }  | ||
         repeat    |          repeat    | ||
             local   |              local part_prefix = string.format('%spart%s_', prefix, part_num)  | ||
             part = {  | |||
                 item_name = tpl_args[  |                  item_name = tpl_args[part_prefix .. 'item_name'],  | ||
                 item_id = tpl_args[  |                  item_id = tpl_args[part_prefix .. 'item_id'],    | ||
                 item_page = tpl_args[  |                  item_page = tpl_args[part_prefix .. 'item_page'],    | ||
                 amount = tonumber(tpl_args[  |                  amount = tonumber(tpl_args[part_prefix .. 'amount']),  | ||
                 notes = m_util.cast.text(tpl_args[  |                  notes = m_util.cast.text(tpl_args[part_prefix .. 'notes']),  | ||
             }  |              }  | ||
             if   |              if part.item_name ~= nil or part.item_id ~= nil or part.item_page ~= nil then  | ||
                 if   |                  if part.amount == nil then  | ||
                     error(string.format(i18n.errors.missing_amount,   |                      error(string.format(i18n.errors.missing_amount, part_prefix .. 'amount'))  | ||
                 else  |                  else  | ||
                     for key, array in pairs(query_data) do  |                      for key, array in pairs(query_data) do  | ||
                         local value =   |                          local value = part['item_' .. key]  | ||
                         if value then  |                          if value then  | ||
                             if array[value] then  |                              if array[value] then  | ||
                                 table.insert(array[value], {  |                                  table.insert(array[value], {recipe_num, part_num})  | ||
                             else  |                              else  | ||
                                 array[value] = {{  |                                  array[value] = {{recipe_num, part_num}, }  | ||
                             end  |                              end  | ||
                         end  |                          end  | ||
                     end  |                      end  | ||
                     recipe.parts[#recipe.parts+1] = part  | |||
                 end  |                  end  | ||
             end  |              end  | ||
             part_num = part_num + 1  | |||
         until   |          until part.item_name == nil and part.item_id == nil and part.item_page == nil  | ||
         --   |          -- recipe was empty, can terminate safely  | ||
         if #  |          if #recipe.parts == 0 then  | ||
             recipe = nil  | |||
         else  |          else  | ||
             recipe_num = recipe_num + 1  | |||
             recipes[#recipes+1] = recipe  | |||
         end  |          end  | ||
     until   |      until recipe == nil  | ||
     -- ------------------------------------------------------------------------  |      -- ------------------------------------------------------------------------  | ||
     -- Automatic  |      -- Automatic  | ||
     -- ------------------------------------------------------------------------  |      -- ------------------------------------------------------------------------  | ||
    local automatic_index = #recipes + 1  | |||
     --  |      --  | ||
     --  maps  |      --  maps  | ||
     --  |      --  | ||
     if tpl_args.class_id == 'Map' and tpl_args.map_tier > 1 and tpl_args.map_tier < 16 then  | |||
         local results = m_cargo.query(  |          local results = m_cargo.query(  | ||
             {'items', 'maps'},  |              {'items', 'maps'},  | ||
             {'items._pageName',   |              {'items._pageName', 'items.name'},  | ||
             {  |              {  | ||
                 join='items._pageID=maps._pageID',  |                  join='items._pageID=maps._pageID',  | ||
                 where=string.format('  |                  where=string.format('maps.tier = %s', tpl_args.map_tier - 1),  | ||
             }  |              }  | ||
         )  |          )  | ||
         for _, row in ipairs(results) do  |          for _, row in ipairs(results) do  | ||
             recipes[#recipes+1] = {  | |||
                 text =   |                  text = nil,  | ||
                 result_amount = 1,  | |||
                parts = {  | |||
                     {  |                      {  | ||
                         item_name = row['items.name'],  |                          item_name = row['items.name'],  | ||
| Line 2,649: | Line 353: | ||
             }  |              }  | ||
         end  |          end  | ||
     end  |      end  | ||
     --  |      --  | ||
     --   |      -- liquid emotions  | ||
     --  |      --  | ||
     if tpl_args._flags.  |      if tpl_args._flags.is_liquid_emotion and tpl_args.liquid_emotion_tier > 1 then  | ||
         local results = m_cargo.query(  |          local results = m_cargo.query(  | ||
             {'items', '  |              {'items', 'liquid_emotions'},  | ||
             {'items._pageName',   |              {'items._pageName', 'items.name'},  | ||
             {  |              {  | ||
                 join='items._pageID=  |                  join='items._pageID=liquid_emotions._pageID',  | ||
                 where=string.format('  |                  where=string.format('liquid_emotions.tier = %s', tpl_args.liquid_emotion_tier - 1),  | ||
             }  |              }  | ||
         )  |          )  | ||
         for _, row in ipairs(results) do  |          for _, row in ipairs(results) do  | ||
             recipes[#recipes+1] = {  | |||
                 text = nil,  |                  text = nil,  | ||
                 result_amount = 1,  | |||
                parts = {  | |||
                     {  |                      {  | ||
                         item_name = row['items.name'],  |                          item_name = row['items.name'],  | ||
| Line 2,679: | Line 384: | ||
     end  |      end  | ||
    --  | |||
    -- runes  | |||
    --  | |||
    if tpl_args.tags and m_util.table.contains(tpl_args.tags, 'rune')  | |||
       and not string.find(tpl_args.metadata_id, 'Lesser')  | |||
       and not string.find(tpl_args.metadata_id, 'RuneSpecial') then  | |||
        -- determine if the runes is greater or normal and get the type  | |||
        local shared_metadata_id = 'Metadata/Items/SoulCores/Rune'  | |||
        local rune_type = string.gsub(tpl_args.metadata_id, shared_metadata_id, '')  | |||
        local is_greater = false  | |||
        if string.find(rune_type, 'Greater') then  | |||
            is_greater = true  | |||
            rune_type = string.gsub(rune_type, 'Greater', '')  | |||
        end  | |||
        -- result  | |||
        local results = m_cargo.query(  | |||
            {'items'},  | |||
            {'items._pageName', 'items.name'},  | |||
            {  | |||
                where=string.format('items.metadata_id = \'%s\'', shared_metadata_id..rune_type..(is_greater and '' or 'Lesser')),  | |||
            }  | |||
        )  | |||
        for _, row in ipairs(results) do  | |||
            recipes[#recipes+1] = {  | |||
                text = nil,  | |||
                result_amount = 1,  | |||
                parts = {  | |||
                    {  | |||
                        item_name = row['items.name'],  | |||
                        item_page = row['items._pageName'],  | |||
                        amount = 3,  | |||
                        notes = nil,  | |||
                    },  | |||
                },  | |||
                automatic = true,  | |||
            }  | |||
        end  | |||
    end  | |||
     --  |      --  | ||
| Line 2,685: | Line 429: | ||
     -- exclude remnant of corruption via type  |      -- exclude remnant of corruption via type  | ||
     if tpl_args.is_essence and tpl_args.essence_type > 0 then    |      if tpl_args._flags.is_essence and tpl_args.essence_type > 0 then    | ||
         local results = m_cargo.query(  |          local results = m_cargo.query(  | ||
             {'items', 'essences'},  |              {'items', 'essences'},  | ||
| Line 2,720: | Line 464: | ||
             if row['essences.category'] == tpl_args.essence_category then  |              if row['essences.category'] == tpl_args.essence_category then  | ||
                 -- 3 to 1 recipe  |                  -- 3 to 1 recipe  | ||
                 recipes[#recipes+1] = {  | |||
                     automatic = true,  |                      automatic = true,  | ||
                    result_amount = 1,  | |||
                     text = nil,  |                      text = nil,  | ||
                     parts = {  | |||
                         {  |                          {  | ||
                             item_id = row['items.metadata_id'],  |                              item_id = row['items.metadata_id'],  | ||
| Line 2,733: | Line 478: | ||
                 }  |                  }  | ||
                 -- corruption +1  |                  -- corruption +1  | ||
                 recipes[#recipes+1] = {  | |||
                     automatic = true,  |                      automatic = true,  | ||
                    result_amount = 1,  | |||
                     text = i18n.essence_plus_one_level,  |                      text = i18n.essence_plus_one_level,  | ||
                     parts = {  | |||
                         {  |                          {  | ||
                             item_id = row['items.metadata_id'],  |                              item_id = row['items.metadata_id'],  | ||
| Line 2,753: | Line 499: | ||
             elseif tonumber(row['essences.type']) == tpl_args.essence_type - 1 then  |              elseif tonumber(row['essences.type']) == tpl_args.essence_type - 1 then  | ||
                 -- corruption type change  |                  -- corruption type change  | ||
                 recipes[#recipes+1] = {  | |||
                     automatic = true,  |                      automatic = true,  | ||
                    result_amount = 1,  | |||
                     text = i18n.essence_type_change,  |                      text = i18n.essence_type_change,  | ||
                     parts = {  | |||
                         {  |                          {  | ||
                             item_id = row['items.metadata_id'],  |                              item_id = row['items.metadata_id'],  | ||
| Line 2,776: | Line 523: | ||
     -- data based on mapping  |      -- data based on mapping  | ||
     if tpl_args.drop_enabled and not tpl_args.  |      if tpl_args.drop_enabled and not tpl_args.disable_automatic_recipes then  | ||
         for   |         -- Test and cache results of all named conditions  | ||
         for k, condition in pairs(c.named_conditions) do  | |||
             if type(condition) == 'function' then  | |||
                c.named_conditions[k] = condition(tpl_args)  | |||
             end  | |||
        end  | |||
        for _, data in ipairs(c.automatic_recipes) do  | |||
            local valid = true -- Can this recipe produce the item?  | |||
            -- Check cached results for named conditions  | |||
            for k, condition in pairs(c.named_conditions) do  | |||
                 if data.conditions[k] then  | |||
                     valid = condition  | |||
                     if not valid then  | |||
                 if   | |||
                     if not   | |||
                         break  |                          break  | ||
                     end    |                      end  | ||
                 end  |                  end  | ||
             end  |              end  | ||
             for _, condition in ipairs(data.  | |||
            -- Test anonymous conditions  | |||
                 if not   |              for _, condition in ipairs(data.conditions) do  | ||
                 valid = condition(tpl_args) and valid  | |||
                 if not valid then  | |||
                     break  |                      break  | ||
                 end  |                  end  | ||
             end  |              end  | ||
             if   |              if valid then  | ||
                 recipes[#recipes+1] = {  | |||
                     automatic = true,  |                      automatic = true,  | ||
                     text = data.text(  |                     result_amount = 1,  | ||
                     text = data.text(),  | |||
                     parts = data.parts,  | |||
                 }  |                  }  | ||
                 for   |                  for part_num, row in ipairs(data.parts) do  | ||
                     if query_data['id'][row.item_id] then  |                      if query_data['id'][row.item_id] then  | ||
                         table.insert(query_data['id'][row.item_id], {#  |                          table.insert(query_data['id'][row.item_id], {#recipes, part_num})  | ||
                     else  |                      else  | ||
                         query_data['id'][row.item_id] = {{#  |                          query_data['id'][row.item_id] = {{#recipes, part_num}, }  | ||
                     end  |                      end  | ||
                 end  |                  end  | ||
| Line 2,823: | Line 570: | ||
     end  |      end  | ||
     if #  |      if #recipes == 0 then  | ||
         return  |          return  | ||
     end  |      end  | ||
     --  |      --  | ||
     -- Fetch item data in a single query to sacrifice database load with a lot of   |      -- Fetch item data in a single query to sacrifice database load with a lot of references  | ||
     --  |      --  | ||
     local query_data_array = {  |      local query_data_array = {  | ||
| Line 2,858: | Line 605: | ||
         }  |          }  | ||
     )  |      )  | ||
     -- Now do The Void  | |||
    for _, row in ipairs(results) do  | |||
        if row[query_fields.id] and string.find(row[query_fields.id], 'Metadata/Items/DivinationCards/', 1, true) then  | |||
            local part = {  | |||
                item_id = 'Metadata/Items/DivinationCards/DivinationCardTheVoid',  | |||
                amount = 1,  | |||
            }  | |||
            local result = m_cargo.query(  | |||
                {'items'},  | |||
                {'items._pageName',  'items.name', 'items.metadata_id'},  | |||
                {  | |||
                    where=string.format('%s = "%s"', query_fields.id, part.item_id),  | |||
                }  | |||
            )  | |||
            if #result > 0 then  | |||
                recipes[#recipes+1] = {  | |||
                    automatic = true,  | |||
                    result_amount = 1,  | |||
                    text = i18n.the_void,  | |||
                    parts = {part},  | |||
                }  | |||
                if query_data['id'][part.item_id] then  | |||
                    table.insert(query_data['id'][part.item_id], {#recipes, 1})  | |||
                else  | |||
                    query_data['id'][part.item_id] = {{#recipes, 1}, }  | |||
                end  | |||
                table.insert(results, result[1])  | |||
            end  | |||
            break  | |||
        end  | |||
    end  | |||
     for _, row in ipairs(results) do  |      for _, row in ipairs(results) do  | ||
         for key, thing_array in pairs(query_data) do  |          for key, thing_array in pairs(query_data) do  | ||
             local   |              local recipe_parts = thing_array[row[query_fields[key]]]  | ||
             if   |              if recipe_parts then  | ||
                 for _,   |                  for _, recipe_part in ipairs(recipe_parts) do  | ||
                     local entry =   |                      local entry = recipes[recipe_part[1]].parts[recipe_part[2]]  | ||
                     for entry_key, data_key in pairs(query_fields) do  |                      for entry_key, data_key in pairs(query_fields) do  | ||
                         -- metadata_id may be nil, since we don't know them for unique items  |                          -- metadata_id may be nil, since we don't know them for unique items  | ||
| Line 2,882: | Line 661: | ||
         -- query data was pruned of existing keys earlier, so only broken keys remain  |          -- query data was pruned of existing keys earlier, so only broken keys remain  | ||
         for key, array in pairs(query_data) do  |          for key, array in pairs(query_data) do  | ||
             for thing,   |              for thing, recipe_parts in pairs(array) do  | ||
                 for _,   |                  for _, recipe_part in ipairs(recipe_parts) do  | ||
                     tpl_args._flags.  |                      tpl_args._flags.invalid_recipe_parts = true  | ||
                     tpl_args._errors[#tpl_args._errors+1] = string.format(i18n.errors.  |                      tpl_args._errors[#tpl_args._errors+1] = m_util.string.format(i18n.errors.invalid_recipe_parts, string.format('recipe%s_part%s_item_%s', recipe_part[1], recipe_part[2], key), thing)  | ||
                 end  |                  end  | ||
             end  |              end  | ||
| Line 2,894: | Line 673: | ||
     -- Check for duplicates  |      -- Check for duplicates  | ||
     --  |      --  | ||
     local   |      local delete_recipes = {}  | ||
     for i=automatic_index, #  |      for i=automatic_index, #recipes do  | ||
         for j=1, automatic_index-1 do  |          for j=1, automatic_index-1 do  | ||
             if #  |              if #recipes[i].parts == #recipes[j].parts then  | ||
                 local match = true  |                  local match = true  | ||
                 for row_id, row in ipairs(  |                  for row_id, row in ipairs(recipes[i].parts) do  | ||
                     -- Only the fields from the database query are matched since we can be sure they're correct. Other fields may be subject to user error.  |                      -- Only the fields from the database query are matched since we can be sure they're correct. Other fields may be subject to user error.  | ||
                     for _, key in ipairs({'item_id', 'item_name', 'item_page'})  do  |                      for _, key in ipairs({'item_id', 'item_name', 'item_page'})  do  | ||
                         match = match and (row[key] ==   |                          match = match and (row[key] == recipes[j].parts[row_id][key])  | ||
                     end  |                      end  | ||
                 end  |                  end  | ||
                 if match then  |                  if match then  | ||
                     tpl_args._flags.  |                      tpl_args._flags.duplicate_recipes = true  | ||
                     tpl_args._errors[#tpl_args._errors+1] = string.format(i18n.errors.  |                      tpl_args._errors[#tpl_args._errors+1] = string.format(i18n.errors.duplicate_recipes, j)  | ||
                     delete_recipes[#delete_recipes+1] = j    | |||
                 end  |                  end  | ||
             end  |              end  | ||
| Line 2,914: | Line 693: | ||
     end  |      end  | ||
     for offset, index in ipairs(  |      for offset, index in ipairs(delete_recipes) do  | ||
         table.remove(  |          table.remove(recipes, index-(offset-1))  | ||
     end  |      end  | ||
     --  |      --  | ||
     -- Set data  |      -- Set data  | ||
     --    |      --    | ||
     tpl_args.  |      tpl_args.recipes = recipes  | ||
     --   |      -- Set recipes data  | ||
     for i,   |      for i, recipe in ipairs(recipes) do  | ||
         table.insert(tpl_args._store_data, {  | |||
             _table = '  |              _table = 'acquisition_recipes',  | ||
             recipe_id = i,  | |||
             result_amount = recipe.result_amount,  | |||
             automatic =   |             description = recipe.text,  | ||
         }  |              automatic = recipe.automatic,  | ||
         })  | |||
         for j,   |          for j, part in ipairs(recipe.parts) do  | ||
             table.insert(tpl_args._store_data, {  | |||
                 _table = '  |                  _table = 'acquisition_recipe_parts',  | ||
                 part_id = j,  | |||
                 recipe_id = i,  | |||
                 item_name =   |                  item_name = part.item_name,  | ||
                 item_id =   |                  item_id = part.item_id,  | ||
                 item_page =   |                  item_page = part.item_page,  | ||
                 amount =   |                  amount = part.amount,  | ||
                 notes =   |                  notes = part.notes,  | ||
             }  |              })  | ||
         end  |          end  | ||
     end  |      end  | ||
| Line 2,950: | Line 730: | ||
--  | --  | ||
function p.debug_validate_auto_upgraded_from(  | function p.debug_validate_auto_upgraded_from()  | ||
     local q = {}  |      local q = {}  | ||
     local chk = {}  |      local chk = {}  | ||
     for _, data in ipairs(c.  |      for _, data in ipairs(c.automatic_recipes) do  | ||
         for _,   |          for _, part in ipairs(data.parts) do  | ||
             q[#q+1] =   |              q[#q+1] = part.item_id  | ||
             chk[  |              chk[part.item_id] = {  | ||
                 amount=  |                  amount=part.amount,  | ||
                 text=data.text(  |                  text=data.text(),  | ||
             }  |              }  | ||
         end  |          end  | ||
| Line 2,981: | Line 759: | ||
     end  |      end  | ||
     tbl = mw.html.create('table')  |      local tbl = mw.html.create('table')  | ||
     tbl:attr('class', 'wikitable sortable')  |      tbl:attr('class', 'wikitable sortable')  | ||
     for _, row in ipairs(results) do  |      for _, row in ipairs(results) do  | ||
Latest revision as of 14:20, 2 September 2025
This submodule of Module:Item contains configuration and functions for item recipes.
The above documentation is transcluded from Module:Item/recipes/doc. 
Editors can experiment in this module's sandbox and testcases pages.
Subpages of this module.
Editors can experiment in this module's sandbox and testcases pages.
Subpages of this module.
-------------------------------------------------------------------------------
-- 
-- Recipes for Module:Item
-- 
-------------------------------------------------------------------------------
local m_util = require('Module:Util')
local m_cargo = require('Module:Cargo')
local m_game = mw.loadData('Module:Game')
-- Lazy loading
local f_modifier_link -- require('Module:Modifier link').modifier_link
-- Should we use the sandbox version of our submodules?
local use_sandbox = m_util.misc.maybe_sandbox('Item')
-- The cfg table contains all localisable strings and configuration, to make it
-- easier to port this module to another wiki.
local cfg = use_sandbox and mw.loadData('Module:Item/config/sandbox') or mw.loadData('Module:Item/config')
local i18n = cfg.i18n.recipes
-- ----------------------------------------------------------------------------
-- Helper functions 
-- ----------------------------------------------------------------------------
local h = {}
-- Lazy loading for Module:Modifier link
function h.modifier_link(args)
    if not f_modifier_link then
        f_modifier_link = require('Module:Modifier link').main
    end
    return f_modifier_link(args)
end
h.conditions = {}
h.conditions.factory = {}
function h.conditions.factory.arg(args)
    -- Required:
    --  arg: The argument to check against
    --  One must be specified
    --   value: check whether the argument equals this value
    --   values: check whether the argument is in this list of values
    --   values_assoc: check whether the argument is in this associative table
    --
    -- Optional:
    --  negate: negates the check against the value, i.e. whether the value is not equal or not in the list/table.
    args = args or {}
    
    -- Inner type of function depending on whether to check a single value, a list of values or an associative list of values
    local inner
    if args.value ~= nil then
        inner = function (tpl)
            return tpl == args.value
        end
    elseif args.values ~= nil then
        inner = function (tpl)
            for _, value in ipairs(args.values) do
                if tpl == value then
                    return true
                end
            end
            return false
        end
    elseif args.values_assoc ~= nil then
        inner = function(tpl) 
            return args.values_assoc[tpl] ~= nil
        end
    else
        error(string.format('Missing inner comparision function. Args: %s', mw.dumpObject(args)))
    end
    
    -- Outer type of function depending on whether to check a single value or against a table
    return function (tpl_args)
        local tpl_value = tpl_args[args.arg]
        local rtr
        if type(tpl_value) == 'table' then
            rtr = false
            for key, value in pairs(tpl_value) do
                if type(key) == 'number' then
                    rtr = rtr or inner(value)
                else
                    rtr = rtr or inner(key)
                end
            end
        else
            rtr = inner(tpl_value)
        end
        if args.negate then
            rtr = not rtr
        end
        return rtr
     end
end
function h.conditions.factory.not_arg(args)
    args = args or {}
    args.negate = true
    return h.conditions.factory.arg(args)
end
function h.conditions.factory.flag_is_set(args)
    return function (tpl_args)
        return tpl_args._flags[args.flag] == true
    end
end
function h.conditions.factory.acquisition_tag(args)
    return function (tpl_args)
        local negate = args.negate or false
        for _, tag in ipairs(tpl_args.acquisition_tags or {}) do
            if tag == args.tag then
                return not negate
            end
        end
        return negate
    end
end
function h.conditions.factory.drop_monsters(args)
    return function (tpl_args)
        for _, monster in ipairs(tpl_args.drop_monsters or {}) do
            if string.find(monster, args.monster, 1, true) then
                return true
            end
        end
        return false
    end
end
function h.conditions.factory.drop_rarity(args)
    return function (tpl_args)
        for _, rarity in ipairs(tpl_args.drop_rarities_ids or {}) do
            if rarity == args.rarity then
                return true
            end
        end
        return false
    end
end
function h.conditions.factory.drop_level_not_greater_than(args)
    return function (tpl_args)
        if tpl_args.drop_level == nil then
            return true
        end
        return tpl_args.drop_level <= args.level
    end
end
function h.conditions.item_class_has_corrupted_implicits(tpl_args)
    local groups = {
        cfg.class_groups.weapons.keys,
        cfg.class_groups.armor.keys,
        cfg.class_groups.jewellery.keys,
        {['Quiver'] = true, ['Jewel'] = true, ['AbyssJewel'] = true},
    }
    for _, g in ipairs(groups) do
        if h.conditions.factory.arg{arg='class_id', values_assoc=g}(tpl_args) then
            return true
        end
    end
    return false
end
function h.conditions.item_class_has_influences(tpl_args)
    local groups = {
        cfg.class_groups.weapons.keys,
        cfg.class_groups.armor.keys,
        cfg.class_groups.jewellery.keys,
        {['Quiver'] = true},
    }
    for _, g in ipairs(groups) do
        if h.conditions.factory.arg{arg='class_id', values_assoc=g}(tpl_args) and h.conditions.factory.not_arg{arg='class_id', value='FishingRod'}(tpl_args) then
            return true
        end
    end
    return false
end
function h.conditions.item_class_has_synthesised_implicits(tpl_args)
    local groups = {
        cfg.class_groups.weapons.keys,
        cfg.class_groups.armor.keys,
        cfg.class_groups.jewellery.keys,
        {['Quiver'] = true, ['Jewel'] = true, ['AbyssJewel'] = true},
    }
    for _, g in ipairs(groups) do
        if h.conditions.factory.arg{arg='class_id', values_assoc=g}(tpl_args) and h.conditions.factory.not_arg{arg='class_id', value='FishingRod'}(tpl_args) then
            return true
        end
    end
    return false
end
function h.conditions.item_class_has_fractured_modifiers(tpl_args)
    local groups = {
        cfg.class_groups.weapons.keys,
        cfg.class_groups.armor.keys,
        cfg.class_groups.jewellery.keys,
        {['Quiver'] = true, ['Jewel'] = true, ['Map'] = true},
    }
    for _, g in ipairs(groups) do
        if h.conditions.factory.arg{arg='class_id', values_assoc=g}(tpl_args) and h.conditions.factory.not_arg{arg='class_id', value='FishingRod'}(tpl_args) then
            return true
        end
    end
    return false
end
-- ----------------------------------------------------------------------------
-- Additional configuration
-- ----------------------------------------------------------------------------
local c = {}
c.named_conditions = {
    is_normal = h.conditions.factory.arg{arg='rarity_id', value='normal'},
    is_unique = h.conditions.factory.arg{arg='rarity_id', value='unique'},
    is_not_drop_restricted = h.conditions.factory.arg{arg='is_drop_restricted', value=false},
    is_not_corrupted = h.conditions.factory.arg{arg='is_corrupted', value=false},
    is_not_replica = h.conditions.factory.arg{arg='is_replica', value=false},
    drop_level_ngt_divcard_default_max_ilvl = h.conditions.factory.drop_level_not_greater_than{level=cfg.divination_card_exchange_default_max_ilvl},
    item_class_has_corrupted_implicits = h.conditions.item_class_has_corrupted_implicits,
    item_class_has_influences = h.conditions.item_class_has_influences,
    item_class_has_synthesised_implicits = h.conditions.item_class_has_synthesised_implicits,
    item_class_has_fractured_modifiers = h.conditions.item_class_has_fractured_modifiers,
}
-- Order matters!
-- Put most specific outcome at the top and the least specific at the bottom.
c.automatic_recipes = {
--[[
    {
        conditions = {
            function (tpl_args) end,
        },
        text = '',
        parts = {
            {
                name = '',
                item_id = '',
                amount = 0,
                notes = '',
            },
        },
    },
]]
    
}
-- ----------------------------------------------------------------------------
-- Exported functions
-- ----------------------------------------------------------------------------
local p = {}
function p.process_recipes(tpl_args)
    local query_data = {
        id = {},
        name = {},
        page = {},
    }
    local recipes = {}
    
    -- ------------------------------------------------------------------------
    -- Manual data
    -- ------------------------------------------------------------------------
    local recipe_num = #recipes + 1
    local recipe
    repeat
        local prefix = string.format('recipe%s_', recipe_num)
        local part_num = 1
        local part
        recipe = {
            parts = {},
            result_amount = tonumber(tpl_args[prefix .. 'result_amount']) or 1,
            text = m_util.cast.text(tpl_args[prefix .. 'description']),
            automatic = false,
        }
        repeat 
            local part_prefix = string.format('%spart%s_', prefix, part_num)
            part = {
                item_name = tpl_args[part_prefix .. 'item_name'],
                item_id = tpl_args[part_prefix .. 'item_id'], 
                item_page = tpl_args[part_prefix .. 'item_page'], 
                amount = tonumber(tpl_args[part_prefix .. 'amount']),
                notes = m_util.cast.text(tpl_args[part_prefix .. 'notes']),
            }
            
            if part.item_name ~= nil or part.item_id ~= nil or part.item_page ~= nil then
                if part.amount == nil then
                    error(string.format(i18n.errors.missing_amount, part_prefix .. 'amount'))
                else
                    for key, array in pairs(query_data) do
                        local value = part['item_' .. key]
                        if value then
                            if array[value] then
                                table.insert(array[value], {recipe_num, part_num})
                            else
                                array[value] = {{recipe_num, part_num}, }
                            end
                        end
                    end
                    recipe.parts[#recipe.parts+1] = part
                end
            end
            
            part_num = part_num + 1
        until part.item_name == nil and part.item_id == nil and part.item_page == nil
        
        -- recipe was empty, can terminate safely
        if #recipe.parts == 0 then
            recipe = nil
        else
            recipe_num = recipe_num + 1
            recipes[#recipes+1] = recipe
        end
    until recipe == nil
    -- ------------------------------------------------------------------------
    -- Automatic
    -- ------------------------------------------------------------------------
    local automatic_index = #recipes + 1
    
    --
    --  maps
    --
    if tpl_args.class_id == 'Map' and tpl_args.map_tier > 1 and tpl_args.map_tier < 16 then
        local results = m_cargo.query(
            {'items', 'maps'},
            {'items._pageName', 'items.name'},
            {
                join='items._pageID=maps._pageID',
                where=string.format('maps.tier = %s', tpl_args.map_tier - 1),
            }
        )
        for _, row in ipairs(results) do
            recipes[#recipes+1] = {
                text = nil,
                result_amount = 1,
                parts = {
                    {
                        item_name = row['items.name'],
                        item_page = row['items._pageName'],
                        amount = 3,
                        notes = nil,
                    },
                },
                automatic = true,
            }
        end
    end
    
    --
    -- liquid emotions
    --
    if tpl_args._flags.is_liquid_emotion and tpl_args.liquid_emotion_tier > 1 then
        local results = m_cargo.query(
            {'items', 'liquid_emotions'},
            {'items._pageName', 'items.name'},
            {
                join='items._pageID=liquid_emotions._pageID',
                where=string.format('liquid_emotions.tier = %s', tpl_args.liquid_emotion_tier - 1),
            }
        )
        for _, row in ipairs(results) do
            recipes[#recipes+1] = {
                text = nil,
                result_amount = 1,
                parts = {
                    {
                        item_name = row['items.name'],
                        item_page = row['items._pageName'],
                        amount = 3,
                        notes = nil,
                    },
                },
                automatic = true,
            }
        end
    end
    
    --
    -- runes
    --
    if tpl_args.tags and m_util.table.contains(tpl_args.tags, 'rune')
       and not string.find(tpl_args.metadata_id, 'Lesser')
       and not string.find(tpl_args.metadata_id, 'RuneSpecial') then
        -- determine if the runes is greater or normal and get the type
        local shared_metadata_id = 'Metadata/Items/SoulCores/Rune'
        local rune_type = string.gsub(tpl_args.metadata_id, shared_metadata_id, '')
        local is_greater = false
        if string.find(rune_type, 'Greater') then
            is_greater = true
            rune_type = string.gsub(rune_type, 'Greater', '')
        end
        
        -- result
        local results = m_cargo.query(
            {'items'},
            {'items._pageName', 'items.name'},
            {
                where=string.format('items.metadata_id = \'%s\'', shared_metadata_id..rune_type..(is_greater and '' or 'Lesser')),
            }
        )
        for _, row in ipairs(results) do
            recipes[#recipes+1] = {
                text = nil,
                result_amount = 1,
                parts = {
                    {
                        item_name = row['items.name'],
                        item_page = row['items._pageName'],
                        amount = 3,
                        notes = nil,
                    },
                },
                automatic = true,
            }
        end
    end
    
    --
    -- essences
    --
    
    -- exclude remnant of corruption via type
    if tpl_args._flags.is_essence and tpl_args.essence_type > 0 then 
        local results = m_cargo.query(
            {'items', 'essences'},
            {
                'items._pageName',  
                'items.name', 
                'items.metadata_id',
                'essences.category',
                'essences.type',
            },
            {
                join='items._pageID=essences._pageID',
                where=string.format([[
                        (essences.category="%s" AND essences.level = %s)
                        OR (essences.type = %s AND essences.level = %s)
                        OR items.metadata_id = 'Metadata/Items/Currency/CurrencyCorruptMonolith'
                        OR (%s = 6 AND essences.type = 5 AND essences.level >= 5) 
                    ]], 
                    tpl_args.essence_category, tpl_args.essence_level - 1, 
                    tpl_args.essence_type - 1, tpl_args.essence_level,
                    -- special case for corruption only essences
                    tpl_args.essence_type
                ),
                orderBy='essences.level ASC, essences.type ASC',
            }
        )
        
        local remnant = results[1]
        if remnant['items.metadata_id'] ~= 'Metadata/Items/Currency/CurrencyCorruptMonolith' then
            error(string.format('Something went seriously wrong here. Got results: %s', mw.dumpObject(results)))
        end
        for i=2, #results do
            local row = results[i]
            if row['essences.category'] == tpl_args.essence_category then
                -- 3 to 1 recipe
                recipes[#recipes+1] = {
                    automatic = true,
                    result_amount = 1,
                    text = nil,
                    parts = {
                        {
                            item_id = row['items.metadata_id'],
                            item_page = row['items._pageName'],
                            item_name = row['items.name'],
                            amount = 3,
                        },
                    },
                }
                -- corruption +1
                recipes[#recipes+1] = {
                    automatic = true,
                    result_amount = 1,
                    text = i18n.essence_plus_one_level,
                    parts = {
                        {
                            item_id = row['items.metadata_id'],
                            item_page = row['items._pageName'],
                            item_name = row['items.name'],
                            amount = 1,
                        },
                        {
                            item_id = remnant['items.metadata_id'],
                            item_page = remnant['items._pageName'],
                            item_name = remnant['items.name'],
                            amount = 1,
                        },
                    },
                }
            elseif tonumber(row['essences.type']) == tpl_args.essence_type - 1 then
                -- corruption type change
                recipes[#recipes+1] = {
                    automatic = true,
                    result_amount = 1,
                    text = i18n.essence_type_change,
                    parts = {
                        {
                            item_id = row['items.metadata_id'],
                            item_page = row['items._pageName'],
                            item_name = row['items.name'],
                            amount = 1,
                        },
                        {
                            item_id = remnant['items.metadata_id'],
                            item_page = remnant['items._pageName'],
                            item_name = remnant['items.name'],
                            amount = 1,
                        },
                    },
                }
            end
        end
    end
    
    -- data based on mapping
    if tpl_args.drop_enabled and not tpl_args.disable_automatic_recipes then
        -- Test and cache results of all named conditions
        for k, condition in pairs(c.named_conditions) do
            if type(condition) == 'function' then
                c.named_conditions[k] = condition(tpl_args)
            end
        end
        for _, data in ipairs(c.automatic_recipes) do
            local valid = true -- Can this recipe produce the item?
            -- Check cached results for named conditions
            for k, condition in pairs(c.named_conditions) do
                if data.conditions[k] then
                    valid = condition
                    if not valid then
                        break
                    end
                end
            end
            -- Test anonymous conditions
            for _, condition in ipairs(data.conditions) do
                valid = condition(tpl_args) and valid
                if not valid then
                    break
                end
            end
            if valid then
                recipes[#recipes+1] = {
                    automatic = true,
                    result_amount = 1,
                    text = data.text(),
                    parts = data.parts,
                }
                for part_num, row in ipairs(data.parts) do
                    if query_data['id'][row.item_id] then
                        table.insert(query_data['id'][row.item_id], {#recipes, part_num})
                    else
                        query_data['id'][row.item_id] = {{#recipes, part_num}, }
                    end
                end
            end
        end
    end
    
    if #recipes == 0 then
        return
    end
    --
    -- Fetch item data in a single query to sacrifice database load with a lot of references
    --
    local query_data_array = {
        id = {},
        name = {},
        page = {},
    }
    local query_fields = {
        id = 'items.metadata_id',
        page = 'items._pageName',
        name = 'items.name',
    }
    local where = {}
    local expected_count = 0
    for key, thing_array in pairs(query_data) do
        for thing, _ in pairs(thing_array) do
            table.insert(query_data_array[key], thing)
        end
        if #query_data_array[key] > 0 then
            expected_count = expected_count + #query_data_array[key]
            local q_data = table.concat(query_data_array[key], '", "')
            table.insert(where, string.format('%s IN ("%s")', query_fields[key], q_data))
        end
    end
    local results = m_cargo.query(
        {'items'},
        {'items._pageName',  'items.name', 'items.metadata_id'},
        {
            where=table.concat(where, ' OR '),
        }
    )
    -- Now do The Void
    for _, row in ipairs(results) do
        if row[query_fields.id] and string.find(row[query_fields.id], 'Metadata/Items/DivinationCards/', 1, true) then
            local part = {
                item_id = 'Metadata/Items/DivinationCards/DivinationCardTheVoid',
                amount = 1,
            }
            local result = m_cargo.query(
                {'items'},
                {'items._pageName',  'items.name', 'items.metadata_id'},
                {
                    where=string.format('%s = "%s"', query_fields.id, part.item_id),
                }
            )
            if #result > 0 then
                recipes[#recipes+1] = {
                    automatic = true,
                    result_amount = 1,
                    text = i18n.the_void,
                    parts = {part},
                }
                if query_data['id'][part.item_id] then
                    table.insert(query_data['id'][part.item_id], {#recipes, 1})
                else
                    query_data['id'][part.item_id] = {{#recipes, 1}, }
                end
                table.insert(results, result[1])
            end
            break
        end
    end
    for _, row in ipairs(results) do
        for key, thing_array in pairs(query_data) do
            local recipe_parts = thing_array[row[query_fields[key]]]
            if recipe_parts then
                for _, recipe_part in ipairs(recipe_parts) do
                    local entry = recipes[recipe_part[1]].parts[recipe_part[2]]
                    for entry_key, data_key in pairs(query_fields) do
                        -- metadata_id may be nil, since we don't know them for unique items
                        if row[data_key] then
                            entry['item_' .. entry_key] = row[data_key]
                        end
                    end
                end
                -- set this to nil for error checking in later step
                thing_array[row[query_fields[key]]] = nil
            end
        end
    end
    
    -- sbow the broken references if needed
    if #results ~= expected_count then
        -- query data was pruned of existing keys earlier, so only broken keys remain
        for key, array in pairs(query_data) do
            for thing, recipe_parts in pairs(array) do
                for _, recipe_part in ipairs(recipe_parts) do
                    tpl_args._flags.invalid_recipe_parts = true
                    tpl_args._errors[#tpl_args._errors+1] = m_util.string.format(i18n.errors.invalid_recipe_parts, string.format('recipe%s_part%s_item_%s', recipe_part[1], recipe_part[2], key), thing)
                end
            end
        end
    end
    
    --
    -- Check for duplicates
    --
    local delete_recipes = {}
    for i=automatic_index, #recipes do
        for j=1, automatic_index-1 do
            if #recipes[i].parts == #recipes[j].parts then
                local match = true
                for row_id, row in ipairs(recipes[i].parts) do
                    -- Only the fields from the database query are matched since we can be sure they're correct. Other fields may be subject to user error.
                    for _, key in ipairs({'item_id', 'item_name', 'item_page'})  do
                        match = match and (row[key] == recipes[j].parts[row_id][key])
                    end
                end
                if match then
                    tpl_args._flags.duplicate_recipes = true
                    tpl_args._errors[#tpl_args._errors+1] = string.format(i18n.errors.duplicate_recipes, j)
                    delete_recipes[#delete_recipes+1] = j 
                end
            end
        end
    end
    
    for offset, index in ipairs(delete_recipes) do
        table.remove(recipes, index-(offset-1))
    end
    --
    -- Set data
    -- 
    tpl_args.recipes = recipes
    
    -- Set recipes data
    for i, recipe in ipairs(recipes) do
        table.insert(tpl_args._store_data, {
            _table = 'acquisition_recipes',
            recipe_id = i,
            result_amount = recipe.result_amount,
            description = recipe.text,
            automatic = recipe.automatic,
        })
        for j, part in ipairs(recipe.parts) do
            table.insert(tpl_args._store_data, {
                _table = 'acquisition_recipe_parts',
                part_id = j,
                recipe_id = i,
                item_name = part.item_name,
                item_id = part.item_id,
                item_page = part.item_page,
                amount = part.amount,
                notes = part.notes,
            })
        end
    end
end
--
-- Debugging
--
function p.debug_validate_auto_upgraded_from()
    local q = {}
    local chk = {}
    for _, data in ipairs(c.automatic_recipes) do
        for _, part in ipairs(data.parts) do
            q[#q+1] = part.item_id
            chk[part.item_id] = {
                amount=part.amount,
                text=data.text(),
            }
        end
    end
    
    local results = m_cargo.array_query{
        tables={'items', 'stackables'},
        fields={'items.name', 'items.class_id', 'items.description', 'stackables.stack_size'},
        id_field='items.metadata_id',
        id_array=q,
        query={
            join='items._pageName=stackables._pageName',
        },
    }
    
    for _, row in ipairs(results) do
        if row['items.class_id'] == 'DivinationCard' and chk[row['items.metadata_id']].amount ~= tonumber(row['stackables.stack_size']) then
            mw.logObject(string.format('Amount mismatch %s, expected %s', row['items.metadata_id'], row['stackables.stack_size']))
        end
    end
    
    local tbl = mw.html.create('table')
    tbl:attr('class', 'wikitable sortable')
    for _, row in ipairs(results) do
        tbl
            :tag('tr')
                :tag('td')
                    :wikitext(row['items.name'])
                    :done()
                :tag('td')
                    :wikitext(chk[row['items.metadata_id']].text)
                    :done()
                :tag('td')
                    :wikitext(row['items.description'])
                    :done()
                :done()
    end
    
    return tostring(tbl)
end
return p