--[[ Copyright (c) 2020 robot256 (MIT License) * Project: Robot256's Library * File: save_restore.lua * Description: Functions for converting between inventory, burner, and grid objects and native Lua table structures. * Functions handle items in arrays of SimpleItemStack tables, with the extra "data" field to store blueprints etc. * Functions include nil checking for both objects and arrays. * Functions to insert items into objects return list of stacks representing the items that could not be inserted. -]] require("util") local __saveGridStacks__ = nil local __saveGrid__ = nil local exportable = {["blueprint"]=true, ["blueprint-book"]=true, ["upgrade-planner"]=true, ["deconstruction-planner"]=true, ["item-with-tags"]=true} local function mergeStackLists(stacks1, stacks2) if not stacks2 then return stacks1 end if not stacks1 then return stacks2 end for _,s in pairs(stacks2) do if not s.count then s.count = 1 end if s.data or s.health or s.durability or s.ammo then table.insert(stacks1, s) else local found = false for _,t in pairs(stacks1) do if not t.count then t.count = 1 end if s.name == t.name and not (t.ammo or t.data or t.durability) then t.count = t.count + s.count found = true break end end if not found then table.insert(stacks1, s) end end end return stacks1 end local function itemsToStacks(items) local stacks = {} if items then for name, count in pairs(items) do table.insert(stacks, {name=name, count=count}) end if #stacks == 0 then stacks = nil end return stacks end end --------------------------------------------------------------- -- Insert Stack Structure into Inventory. -- Arguments: source -> LuaInventory to save contents of -- Returns: stacks -> Dictionary [slot#] -> SimpleItemStack with extra optional field "data" storing blueprint export string --------------------------------------------------------------- local function saveInventoryStacks(source) if source and source.valid and not source.is_empty() then local stacks = {} for slot = 1, #source do local stack = source[slot] if stack and stack.valid_for_read then if exportable[stack.name] then table.insert(stacks, {name=stack.name, count=1, data=stack.export_stack()}) else local s = {name=stack.name, count = stack.count} if stack.prototype.magazine_size then if stack.ammo < stack.prototype.magazine_size then s.ammo = stack.ammo end end if stack.prototype.durability then if stack.durability < stack.prototype.durability then s.durability = stack.durability end end if stack.health < 1 then s.health = stack.health end -- Merge with existing stacks to avoid duplicates mergeStackLists(stacks, {s}) -- Can't restore equipment to an item's grid, have to unpack it to the inventory if stack.grid and stack.grid.valid then local equipStacks, fuelStacks = __saveGridStacks__(__saveGrid__(stack.grid)) mergeStackLists(stacks, equipStacks) mergeStackLists(stacks, fuelStacks) end end end end return stacks end end --------------------------------------------------------------- -- Insert Stack Structure into Inventory. -- Arguments: target -> LuaInventory to insert items into -- stack -> SimpleItemStack with extra optional field "data" storing blueprint export string. -- stack_limit (optional) -> integer maximum number of items to insert from the given stack. -- Returns: remainder -> SimpleItemStack with extra field "data", representing all the items that could not be inserted at this time. --------------------------------------------------------------- local function insertStack(target, stack, stack_limit) local proto = game.item_prototypes[stack.name] if not stack.count then stack.count = 1 end local remainder = table.deepcopy(stack) if proto then if target.can_insert(stack) then if stack.data then -- Insert bp item, find ItemStack, import data string for i = 1, #target do if not target[i].valid_for_read then -- this stack is empty, set it to blueprint target[i].set_stack(stack) target[i].import_stack(stack.data) return nil -- no remainders after insertion end end else -- Handle normal item, break into chunks if need be, correct for oversized stacks if not stack_limit then stack_limit = math.huge end local d = 0 if stack.count > stack_limit then -- This time we limit ourselves to part of the given stack. d = target.insert({name=stack.name, count=stack_limit}) else -- Only the last part gets assigned ammo and durability ratings of the original stack d = target.insert(stack) end remainder.count = stack.count - d if remainder.count == 0 then return nil -- All items inserted, no remainder else return remainder -- Not all items inserted, return remainder with original ammo/durability ratings end end else -- Can't insert this stack, entire thing is remainder. return remainder end else -- Prototype for this item was removed from the game, don't give a remainder. return nil end end --------------------------------------------------------------- -- Spill Stack Structure onto ground. -- Arguments: target -> LuaInventory to insert items into -- stack -> SimpleItemStack with extra optional field "data" storing blueprint export string. -- stack_limit (optional) -> integer maximum number of items to insert from the given stack. -- Returns: remainder -> SimpleItemStack with extra field "data", representing all the items that could not be inserted at this time. --------------------------------------------------------------- local function spillStack(stack, surface, position) if stack then surface.spill_item_stack(position, stack) if stack.data then -- This is a bp item, find it on the surface and restore data for _,entity in pairs(surface.find_entities_filtered{name="item-on-ground",position=position,radius=1000}) do -- Check if these are the droids we are looking for if entity.stack.valid_for_read then local es = entity.stack if es.name == stack.name then -- TODO: Handle detection of empty deconstruction_planner, upgrade_planner, item_with_tags if es.is_blueprint and not es.is_blueprint_setup() then -- New empty blueprint, let's import into it es.import_stack(stack.data) break elseif es.is_blueprint_book then local estring = es.export_stack() -- Compare export string to empty blueprint book if estring == "0eNqrVkrKKU0tKMrMK4lPys/PVrKqVsosSc1VskJI6IIldJQSk0syy1LjM/NSUiuUrAx0lMpSi4oz8/OUrIwsDE3MLY3MDc3MzS0tjWprAVWnHQo=" then es.import_stack(stack.data) break end elseif es.is_upgrade_item then local estring = es.export_stack() -- Compare export string to empty upgrade planner if estring == "0eNo1yk0KgCAQBtC7fGtbKNGklwmhQQSbxJ824t1b+dZvoOdQ/M1XTl6EC9xA5daihAonPSWF2PiBW3NbU+HjUuMrcObUO1lD+iCy1sz5A4aSHcM=" then es.import_stack(stack.data) break end elseif es.is_deconstruction_item then local estring = es.export_stack() -- Compare export string to empty deconstruction planner if estring == "0eNpdy8EKgCAMANB/2dkOSmTuZyJshGAz3Owi/XtdunR98DpsFAuL1hY1FV7OvDJTBewgpJp4F0BuORtISgfgLwxfMHBRlVcA3WxHH5y3k/chuPt+ALDXI9s=" then es.import_stack(stack.data) break end elseif es.is_item_with_tags then if not es.tags or table_size(es.tags) == 0 then es.import_stack(stack.data) break end end end end end end end end local function spillStacks(stacks, surface, position) if stacks then for _,s in pairs(stacks) do spillStack(s, surface, position) end end end --------------------------------------------------------------- -- Restore Inventory Stack List to Inventory. -- Arguments: target -> LuaInventory to insert items into -- stacks -> List of SimpleItemStack with extra optional field "data" storing blueprint export string. -- Returns: remainders -> List of SimpleItemStacks representing all the items that could not be inserted at this time. --------------------------------------------------------------- local function insertInventoryStacks(target, stacks) local remainders = {} if target and target.valid and stacks then for _,stack in pairs(stacks) do local r = insertStack(target, stack) if r then table.insert(remainders, r) end end elseif stacks then -- If inventory invalid, return entire contents return stacks end if #remainders > 0 then return remainders else return nil end end local function saveBurner(burner) if burner and burner.valid then local saved = {heat = burner.heat} if burner.currently_burning then saved.currently_burning = burner.currently_burning.name saved.remaining_burning_fuel = burner.remaining_burning_fuel end if burner.inventory and burner.inventory.valid then saved.inventory = itemsToStacks(burner.inventory.get_contents()) end if burner.burnt_result_inventory and burner.burnt_result_inventory.valid then saved.burnt_result_inventory = itemsToStacks(burner.burnt_result_inventory.get_contents()) end return saved end end local function restoreBurner(target, saved) if target and target.valid and saved then -- Only restore burner heat if the fuel prototype still exists and is valid in this burner. if (saved.currently_burning and game.item_prototypes[saved.currently_burning] and target.inventory.can_insert({name=saved.currently_burning, count=1})) then target.currently_burning = game.item_prototypes[saved.currently_burning] target.remaining_burning_fuel = saved.remaining_burning_fuel target.heat = saved.heat end local r1 = insertInventoryStacks(target.inventory, saved.inventory) local r2 = insertInventoryStacks(target.burnt_result_inventory, saved.burnt_result_inventory) return mergeStackLists(r1, r2) elseif saved then -- Return entire contents if target invalid local r = mergeStackLists({}, saved.burnt_result_inventory) r = mergeStackLists(r, saved.inventory) return r end end local function saveGrid(grid) if grid and grid.valid then local gridContents = {} for _,v in pairs(grid.equipment) do local item = {name=v.name,position=v.position} local burner = saveBurner(v.burner) local energy, shield if v.energy > 0 then energy = v.energy end if v.shield > 0 then shield = v.shield end table.insert(gridContents, {item=item, energy=energy, shield=shield, burner=burner}) end return gridContents else return nil end end __saveGrid__ = saveGrid local function restoreGrid(grid, savedGrid) local r_stacks = {} if grid and grid.valid and savedGrid then -- Insert as much as possible into this grid, return items not inserted as remainder stacks for _,v in pairs(savedGrid) do if game.equipment_prototypes[v.item.name] then local e = grid.put(v.item) if e then if v.energy then e.energy = v.energy end if v.shield and v.shield > 0 then e.shield = v.shield end if v.burner then local r1 = restoreBurner(e.burner,v.burner) r_stacks = mergeStackLists(r_stacks, r1) end else r_stacks = mergeStackLists(r_stacks, {{name=v.item.name, count=1}}) if v.burner then r_stacks = mergeStackLists(r_stacks, v.burner.inventory) r_stacks = mergeStackLists(r_stacks, v.burner.burnt_result_inventory) end end end end if #r_stacks > 0 then return r_stacks end elseif savedGrid then -- If grid is invalid but we have saved items, return the whole grid as a remainder local e,f = __saveGridStacks__(savedGrid) r_stacks = mergeStackLists(r_stacks, e) r_stacks = mergeStackLists(r_stacks, f) return r_stacks end end local function removeStackFromSavedGrid(savedGrid, stack) if savedGrid and stack then if not stack.count then stack.count = 1 end for i,e in pairs(savedGrid) do if e.item.name == stack.name then savedGrid[i] = nil stack.count = stack.count - 1 elseif e.burner then if e.burner.inventory then for k,s in pairs(e.burner.inventory) do if s.name == stack.name then if s.count <= stack.count then stack.count = stack.count - s.count e.burner.inventory[k] = nil else s.count = s.count - stack.count stack.count = 0 end end end end end if stack.count == 0 then return end end end end --------------------------------------------------------------- -- Convert Grid Contents to SimpleItemStack array. -- Arguments: grid -> table result of saveGrid() -- dest (optional) -> SimpleItemStack array to insert stacks into -- Returns: stacks -> List of SimpleItemStacks or reference to dest --------------------------------------------------------------- local function saveGridStacks(savedGrid) -- Count item numbers so we can add stacks of equipment and fuel properly local items = {} local fuel_items = {} if savedGrid then for _,v in pairs(savedGrid) do if v.burner then fuel_items = mergeStackLists(fuel_items, v.burner.inventory) fuel_items = mergeStackLists(fuel_items, v.burner.burnt_result_inventory) end items = mergeStackLists(items, {{name=v.item.name, count=1}}) end return items, fuel_items end end __saveGridStacks__ = saveGridStacks local function saveFilters(source) local filters = nil if source and source.valid then if source.is_filtered() then filters = {} for f = 1, #source do filters[f] = source.get_filter(f) end end if source.supports_bar() and source.get_bar() <= #source then filters = filters or {} filters.bar = source.get_bar() end end return filters end local function restoreFilters(target, filters) if target and target.valid and filters then if target.supports_filters() then for f = 1, #target do target.set_filter(f, filters[f]) end end if target.supports_bar() then if not filters.bar then target.set_bar() else target.set_bar(filters.bar) -- if filters.bar is nil, will clear bar setting end end end end local function saveItemRequestProxy(target) -- Search for item_request_proxy ghosts targeting this entity local proxies = target.surface.find_entities_filtered{ name = "item-request-proxy", force = target.force, position = target.position } for _, proxy in pairs(proxies) do if proxy.proxy_target == target and proxy.valid then local items = {} local exists = false for k,v in pairs(proxy.item_requests) do items[k] = v exists = true end if exists then return items else return nil end else return nil end end end return { saveBurner = saveBurner, restoreBurner = restoreBurner, saveGrid = saveGrid, restoreGrid = restoreGrid, saveGridStacks = saveGridStacks, removeStackFromSavedGrid = removeStackFromSavedGrid, saveInventoryStacks = saveInventoryStacks, insertStack = insertStack, insertInventoryStacks = insertInventoryStacks, mergeStackLists = mergeStackLists, itemsToStacks = itemsToStacks, spillStack = spillStack, spillStacks = spillStacks, saveFilters = saveFilters, restoreFilters = restoreFilters, saveItemRequestProxy = saveItemRequestProxy, }