Recipe woes

Place to get help with not working mods / modding interface.
Post Reply
dee-
Filter Inserter
Filter Inserter
Posts: 416
Joined: Mon Jan 19, 2015 9:21 am
Contact:

Recipe woes

Post by dee- »

Hi,

I'm currently trying to implement some kind of recycler which takes items and cracks them into their components. Then cracks these again until whitelisted or "raw" items (wood, ore) without a recipe are found.

Now that 0.11.16 is finally here ^^ I tried out LuaRecipe.ingredients.
As described in the wiki It gives me a nice list of the entity's ingredients, much to my delight.

What it however does not show is a, let's call it "gives amount of", attribute. For example this is necessary with the copper wire: 1x copper plate gives 2x copper wire
The ingredients tells me for one copper wire it's composed of 1 copper plate, which is not what the game recipe is, as you get two copper wire for one copper plate.
So it should be 0,5 copper plate as an ingredient for the copper wire or an additional "gives amount of" attribute like there is for PrototypeRecipe: result_count

Edit: okay, found this one: LuaRecipe.products[n].amount but why is this structure so different than in PrototypeRecipe?

Then I totally lost it and am questioning now what this Lua/* Prototype/* is all about.
I have questions that hopefully someone can answer to get the model I'm builing in my head, how this is supposed to work, raised.

1) why is there a Lua/Recipe and a Prototype/Recipe?
1a) both seem to try to accomplish the same, give similar information (ingredients) but they are still so different (result_count, energy)
1b) one is not castable into the other, they are AFAICS not connected in any way

2) why are there prototypes at all?
2a) like 3c above - what the either bring above the other, why is there a Lua/* and a Prototype/* ? I can't see what this is needed for

3) strings to objects
3a) often the exposed item information is a string, e.g. the item type is e.g. "iron-plate". Sometimes it's an object, see the event handlers. I assume this has been done to save memory and just say "this chest has 400x iron-plate" than keeping 400 entity objects.
3b) Probably fair enough but how do I get an Entity froma string, e.g. to get it's recipe?
3c) Currently I'm using "local aItemToCrackRecipe = game.player.force.recipes[cargItemToCrackName]" but this does not seem right to me and gives me (only) LuaRecipe, where no "result_count" is given.

4) where is the crafting time stored?
4a) neither LuaRecipe nor ProtoypeRecipe indicate how much time crafting from a recipe takes. I would have expected an attribute (or hackishly a time ingredient) here for this?

Confused.

Flux Faraday
Long Handed Inserter
Long Handed Inserter
Posts: 66
Joined: Sat May 10, 2014 8:48 am
Contact:

Re: Recipe woes

Post by Flux Faraday »

I think all the data you want is in data.raw.recipe. It might be easier to walk that table after it is built to construct your item cracking list.
[edit] Ignore me. It looks like the data you want is in all three places.

dee-
Filter Inserter
Filter Inserter
Posts: 416
Joined: Mon Jan 19, 2015 9:21 am
Contact:

Re: Recipe woes

Post by dee- »

Flux Faraday wrote:I think all the data you want is in data.raw.recipe. It might be easier to walk that table after it is built to construct your item cracking list.
[edit] Ignore me. It looks like the data you want is in all three places.
I already have the "cracking" working:

Code: Select all

--[[!
    Contains the central logic for the homebrew modified "Barter" mod:
    - all event handlers
    - iteration of export chests
    - cracking exported items into their ingredients with "don't crack"-whitelists and individual trade bonus
    - storing the cracked items into the available import chests
    - fluids are discarded when cracked so probably whitelist the item type it's an ingredient of
    
    Attention: needs at least Factorio version 0.11.16
    
    @author dee-, 15.02.2015
    
    @change 15.02.2015, dee-, rewrote the mod to be compatible with 0.11, added tons of comments, renamed variables, indenting, etc pp., several other code fixes
    @change 20.02.2015, dee-, updated for the new LuaRecipe functionality added in 0.11.16 to get the entity's ingredients, dropped previous logic and data of the cost_table.
                              This probably makes this mod now compatible to ALL other mods and allows cracking of mod items
    @change 20.02.2015, dee-, trade bonus and whitelist added, bonus defaults to 1.01 (1% gain)
]]--



----- helper functions -----



--! flag to indicate if we are developing (true) or productive (false)
local bIsDebug = true

--[[!
    Prints a debug message to the game window.

    @param cMessage the message to show
]]--
function debugLog(cMessage)
    if bIsDebug then
        game.player.print(math.floor(game.tick / 60) .. " : " .. cMessage)
    end
end

--[[!
    Prints a localized string to the game window.

    @param cStringkey key of the string
]]--
function localizedPrint(cStringkey)
    game.player.print({cStringkey})
end



----- imports -----



-- standard types, etc.
require "defines"



----- general attributes -----



--! how many ticks are between each attempt to barter
local barterPeriod = 12500
if bIsDebug then
    barterPeriod = 1000
end



----- hook events -----



-- initialization
game.oninit(function() onInit() end)
game.onload(function() onLoad() end)

-- every tick
game.onevent(defines.events.ontick, function(event) onTick(event) end)

-- when an entity was placed on the map
game.onevent(defines.events.onbuiltentity,      function(event) entityWasPlaced(event) end)
game.onevent(defines.events.onrobotbuiltentity, function(event) entityWasPlaced(event) end)

-- when an entity was removed from the map
game.onevent(defines.events.onentitydied,         function(event) entityWasRemoved(event) end)
game.onevent(defines.events.onpreplayermineditem, function(event) entityWasRemoved(event) end)
game.onevent(defines.events.onrobotpremined,      function(event) entityWasRemoved(event) end)



----- crack down recipes -----



--! multiplicator for the trade bonus, 1: no bonus, <1: negative bonus, >1: positive bonus. Default is 1% gain (= 1.01)
--! remember the final amount is rounded down, so anything lower 1 will cost for sure at least one item as 1 * 0.99 = 0.99, rounded down to 0
local nDefaultTradeBonus = 1.01

--! special multiplicators, given in e.g. '["explosives"] = 2' to double the returned amount of explosives rather than using the default multiplier
local aSpecialTradeBonus = {}

--! do not crack these items. If you want to add/remove items, add/remove the item name here, the number can be anything
local aWhitelistedItems = {
    ["battery"]         = 0,
    ["explosives"]      = 0,
    ["plastic-bar"]     = 0,
    ["processing-unit"] = 0,
    ["sulfur"]          = 0
}

--[[!
    Adds the given amount of given item type to the array.
    If the item type is already present, the amount is added to the stored amount, if not a new key is created with the amount
    
    @param aargTable    the table to modify
    @param cargItemName the item type
    @param nargAmount   the amount to add (or substract when negative)
--]]
function addItemsToTable(aargTable, cargItemName, nargAmount)
    --debugLog("addItemsToTable: adding " .. tostring(nargAmount) .. "x " .. cargItemName)
    if (aargTable[cargItemName]) then
        aargTable[cargItemName] = aargTable[cargItemName] + nargAmount
    else
        aargTable[cargItemName] = nargAmount
    end
end

--[[!
    Cracks down the given amount of given item type into it's ingredients, recursively until only base materials or whitelisted items are left.
    
    @param aargBaseResources    contains the base items found so far. This array will be modified and new base items added to it when cracking
    @param cargItemToCrackName  name of the item to crack, e.g. "copper-wire"
    @param nargItemToCrackCount the amount of the items to crack, serves as multiplicator
]]--
function crackDownToBaseResources(aargBaseResources, cargItemToCrackName, nargItemToCrackCount)
    --debugLog("crackDownToBaseResources: trying to crack " .. tostring(nargItemToCrackCount) .. "x " .. cargItemToCrackName)
    
    -- get the trade bonus for this item
    local nTradeBonus = aSpecialTradeBonus[cargItemToCrackName]
    
    -- was no trade bonus specified?
    if not nTradeBonus then
        -- then use the default bonus
        nTradeBonus = nDefaultTradeBonus
    end
    
    -- is this a whitelisted item?
    if aWhitelistedItems[cargItemToCrackName] then
        -- yes, then don't crack it but just add it to the result and exit
        --debugLog("crackDownToBaseResources: no cracking: whitelisted item");
        
        -- add it to the result array
        local nAmount = nargItemToCrackCount * nTradeBonus
        addItemsToTable(aargBaseResources, cargItemToCrackName, nAmount)
        
        -- we're done here
        return
    end
    
    -- get the recipe of the item
    local aItemToCrackRecipe = game.player.force.recipes[cargItemToCrackName]
    
    -- no recipe?
    if not aItemToCrackRecipe then
        -- no recipe: base item
        --debugLog("crackDownToBaseResources: no cracking: no recipe, base item");
        
        -- add it to the result array
        local nAmount = nargItemToCrackCount * nTradeBonus
        addItemsToTable(aargBaseResources, cargItemToCrackName, nAmount)
        
        -- we're done here
        return
    end
    
    -- how many products you get when using this recipe. Attention: it always uses the first recipe
    local nProductAmount = aItemToCrackRecipe.products[1].amount
    --debugLog("crackDownToBaseResources: cracking with recipe: " .. aItemToCrackRecipe.name .. " (which gave " .. tostring(nProductAmount) .. " of these)")
    
    -- go through all ingredients
    for nCurrentIngredientIndex, aCurrentIngredient in pairs(aItemToCrackRecipe.ingredients) do
        -- non-items like fluids are dumped
        if (aCurrentIngredient.type ~= 0) then 
            debugLog("crackDownToBaseResources: dumping non-item " .. aCurrentIngredient.name)
        else
            -- how many ingredients you need when using this recipe
            local nAmount = nargItemToCrackCount * (aCurrentIngredient.amount / nProductAmount)
        
            -- crack this ingredient and add it to the result
            crackDownToBaseResources(aargBaseResources, aCurrentIngredient.name, nAmount)
        end
    end
end



----- barter logic -----



--[[!
    Gets called when a barter is to be made.
]]--
function performBarter()
    debugLog("-- performBarter --")
    debugLog("number of import chests: " .. tostring(#glob.barterImportChests))
    debugLog("number of export chests: " .. tostring(#glob.barterExportChests))

    -- get the first of our import chests
    local nCurrentImportChestIndex, oCurrentImportChest = next(glob.barterImportChests, nil)

    -- no import chest?
    if not oCurrentImportChest then
        -- we're done
        return
    end

    -- flag which indicates if we could sell something
    local bSoldSomething = false

    -- flag which indicates when all our import chests ran out of space
    local bOverflow = false

    -- flag to show if we encountered unknown/unlisted items to trade
    -- FINDME: to re-add?
    local bUnknownItemsFound = false

    -- iterate through all export chests
    for nCurrentExportChestIndex, oCurrentExportChest in ipairs(glob.barterExportChests) do
        -- only if this entity is valid
        if oCurrentExportChest.valid then
            --debugLog("processing export chest at index " .. tostring(nCurrentExportChestIndex))

            -- get the current export chest's inventory
            local oECInventory = oCurrentExportChest.getinventory(defines.inventory.chest)

            -- get the inventory's contents
            local oECContents = oECInventory.getcontents()

            -- iterate through every item stack in the chest
            for cItemToTradeName, nItemToTradeCount in pairs(oECContents) do
                -- will contain all exchanged items of all recipes of all traded items
                local aTradedForThese = {}
                
                debugLog("trading " .. tostring(nItemToTradeCount) .. "x " .. cItemToTradeName .. " for...")
                crackDownToBaseResources(aTradedForThese, cItemToTradeName, nItemToTradeCount)

                -- start again at the first import chest to minimize non-full stacks across the chests when trading different types of items
                nCurrentImportChestIndex, oCurrentImportChest = next(glob.barterImportChests, nil)
                    
                -- iterate through each item type we receive for selling this item type
                for cTradedForItemName, cTradedForItemCount in pairs(aTradedForThese) do
                    -- there was a trade
                    bSoldSomething = true
                    
                    -- how many do we get for this
                    local nTotal = math.floor(cTradedForItemCount)
                    debugLog("... " .. tostring(nTotal) .. "x " .. cTradedForItemName)

                    -- while there is still something to insert
                    while (nTotal > 0) do
                        -- try to insert in packs of 50 as this is the highest stack size for ore
                        local nInsertLimit = 50

                        -- check if there is less available
                        local nToInsert = nInsertLimit
                        if (nTotal < nInsertLimit) then
                            nToInsert = nTotal
                        end

                        -- can we insert a pack into the current import chest?
                        if oCurrentImportChest.caninsert{name=cTradedForItemName, count=nToInsert} then
                            -- then do it
                            oCurrentImportChest.insert{name=cTradedForItemName, count=nToInsert}

                            -- account
                            nTotal = nTotal - nToInsert
                        else
                            --debugLog("the current import chest got full, switching to the next import chest")

                            -- the current import chest got full, switch to the next import chest
                            nCurrentImportChestIndex, oCurrentImportChest = next(glob.barterImportChests, nCurrentImportChestIndex)
                            --debugLog("new import chest is at index " .. tostring(nCurrentImportChestIndex))

                            -- there is no next import chest?
                            if not oCurrentImportChest then
                                -- this means overflow, so indicate and break
                                debugLog("there is no next import chest -> overflow")
                                bOverflow = true
                                break
                            end
                        end
                    end

                    -- no space left?
                    if bOverflow then
                        break
                    end
                end

                -- remove all of the traded items from the export chest. Attention: this will remove more items than we got stuff for if the import chests overflew
                --debugLog("removing " .. tostring(nItemToTradeCount) .. "x " .. cItemToTradeName .. " from export chest");
                oECInventory.remove{name=cItemToTradeName, count=nItemToTradeCount}

                -- no space left?
                if bOverflow then
                    break
                end
            end
        end

        -- no space left?
        if bOverflow then
            break
        end
    end

    -- say something nice
    if bOverflow then
        localizedPrint("barter-overflow")
    else
        if bUnknownItemsFound then
            if bSoldSomething then
                localizedPrint("barter-successful-invitems")
            else
                localizedPrint("barter-invitems")
            end
        else
            if bSoldSomething then
                localizedPrint("barter-successful")
            end
        end
    end
end



----- event handlers -----



--[[!
    Gets called when the game starts.
    Creates internal data structures when this mod is added for the first time.
]]--
function onInit()
    --debugLog("-- onInit --")

    -- is the mod not already active in the savegame?
    if not glob.nextBarter then
        -- determine when we will barter for the first time
        glob.nextBarter = game.tick + barterPeriod

        -- initialize the internal data structures which will contain our import and export chests
        glob.barterImportChests = {}
        glob.barterExportChests = {}
    end
end

--[[!
    Gets called when the scenario is loaded.
    Does the same as our onInit().

    @see onInit
]]--
function onLoad()
    --debugLog("-- onLoad --")
    onInit()
end

--[[!
    Gets called every tick.
    Checks if a barter should happen.

    @param event the firing event
]]--
function onTick(event)
    -- has the game's tick reached a barter?
    if (game.tick >= glob.nextBarter) then
        -- yes, calculate a new tick when to barter
        glob.nextBarter = glob.nextBarter + barterPeriod

        -- and let's trade
        performBarter()
    end
end

--[[!
    Gets called when a new entity was placed, e.g. by the player or by a bot.

    @param event the firing event
]]--
function entityWasPlaced(event)
    --debugLog("-- entityWasPlaced --")
    local oEntity = event.createdentity

    -- was it an import chest?
    if ((oEntity.name == "import-chest") or (oEntity.name == "import-provider-chest")) then
        -- then add the import chest to the data structure
        debugLog("adding new import chest: " .. oEntity.name)
        table.insert(glob.barterImportChests, oEntity)
        debugLog("there are now " .. tostring(#glob.barterImportChests) .. " import chests")
    end

    -- was it an export chest?
    if ((oEntity.name == "export-chest") or (oEntity.name == "export-requester-chest")) then
        -- then add the export chest to the data structure
        debugLog("adding new export chest: " .. oEntity.name)
        table.insert(glob.barterExportChests, oEntity)
        debugLog("there are now " .. tostring(#glob.barterExportChests) .. " export chests")
    end
end

--[[!
    Gets called when an existing entity was removed, e.g. by the player or by a bot or when it has been destroyed.

    @param event the firing event
]]--
function entityWasRemoved(event)
    --debugLog("-- entityWasRemoved --")
    local oEntity = event.entity

    -- was it an import chest?
    if ((oEntity.name == "import-chest") or (oEntity.name == "import-provider-chest")) then
        -- find the affected chest in the data structure
        for i = 1, #glob.barterImportChests do
            if (glob.barterImportChests[i].equals(oEntity)) then
                -- remove it from the import chest data structure
                debugLog("removing import chest at index " .. tostring(i) .. ": " .. oEntity.name)
                table.remove(glob.barterImportChests,i)
                debugLog("there are now " .. tostring(#glob.barterImportChests) .. " import chests")

                -- done, leave the loop
                break
            end
        end
    end

    -- was it an export chest?
    if ((oEntity.name == "export-chest") or (oEntity.name == "export-requester-chest")) then
        -- find the affected chest in the data structure
        for i = 1, #glob.barterExportChests do
            if (glob.barterExportChests[i].equals(oEntity)) then
                -- remove it from the export chest data structure
                debugLog("removing export chest at index " .. tostring(i) .. ": " .. oEntity.name)
                table.remove(glob.barterExportChests,i)
                debugLog("there are now " .. tostring(#glob.barterExportChests) .. " export chests")

                -- done, leave the loop
                break
            end
        end
    end
end
but I still don't know where to get the recipe duration from

Choumiko
Smart Inserter
Smart Inserter
Posts: 1352
Joined: Fri Mar 21, 2014 10:51 pm
Contact:

Re: Recipe woes

Post by Choumiko »

dee- wrote:but I still don't know where to get the recipe duration from
According to https://forums.factorio.com/wiki/inde ... ype/Recipe it's energy_required, don't know if it's accessible via lua though

Flux Faraday
Long Handed Inserter
Long Handed Inserter
Posts: 66
Joined: Sat May 10, 2014 8:48 am
Contact:

Re: Recipe woes

Post by Flux Faraday »

Choumiko wrote:don't know if it's accessible via lua though
It is. I put this in data.lua after all the require statements:

Code: Select all

print(data.raw.recipe["speed-module"].energy_required)
print(data.raw.recipe["speed-module-2"].energy_required)
print(data.raw.recipe["speed-module-3"].energy_required)
and got
15
30
60

dee-
Filter Inserter
Filter Inserter
Posts: 416
Joined: Mon Jan 19, 2015 9:21 am
Contact:

Re: Recipe woes

Post by dee- »

Oh, I am so blind... must have skipped it because of the name.

Thanks! :)

Edit:
the code line

Code: Select all

local oItemToCrackRecipe = data.raw.recipe[cargItemToCrackName]
gives me:

Code: Select all

Error while running the event handler: __barter__/control.lua:164: attempt to index global 'data' (a nil value)
what did I do wrong?

Flux Faraday
Long Handed Inserter
Long Handed Inserter
Posts: 66
Joined: Sat May 10, 2014 8:48 am
Contact:

Re: Recipe woes

Post by Flux Faraday »

dee- wrote:what did I do wrong?
I don't know the answer to that one, I'm still too new to this. data.raw... is visible from data.lua, but not from control.lua. There might be a way to make it so, but I don't know if/what it is.

Post Reply

Return to “Modding help”