Add a Defragment button to Milo #36
@@ -291,6 +291,134 @@ function Storage:listItems(throttle)
|
||||
return self.cache
|
||||
|
|
||||
end
|
||||
|
||||
-- provide a raw list of all the items in all storage chests
|
||||
-- it might be beneficial to move this to the adapter class at some point and cache the raw item list in this class at some point
|
||||
function Storage:listItemsRaw(throttle)
|
||||
local res = {}
|
||||
|
||||
throttle = throttle or Util.throttle()
|
||||
|
||||
for _, v in pairs(self.nodes) do
|
||||
if v.category == "storage" then
|
||||
|
Need to check if a chest is locked to specific items. There's 2 different ways to go here:
My vote is for #2 :) Need to check if a chest is locked to specific items. There's 2 different ways to go here:
1. the easy way - ignore locked chests.
2. process the locked chests first - only pulling items in that match the filter.
My vote is for #2 :)
Just so I get this right, when a chest is locked it would be enough to prioritise that chest when trying to find a target for the item? Just so I get this right, when a chest is locked it would be enough to prioritise that chest when trying to find a target for the item?
|
||||
local chest = device[v.name]
|
||||
local items = chest.list()
|
||||
|
||||
for slot, item in pairs(items) do
|
||||
items[slot] = itemDB:get(item, function() return chest.getItemMeta(slot) end)
|
||||
end
|
||||
|
||||
res[v.name] = items
|
||||
throttle()
|
||||
end
|
||||
end
|
||||
|
||||
return res
|
||||
end
|
||||
|
||||
-- provide a list of items and which chests provide them
|
||||
function Storage:listProviders(throttle)
|
||||
local res = {}
|
||||
|
||||
local rawItems = self:listItemsRaw(throttle)
|
||||
|
||||
for chest, items in pairs(rawItems) do
|
||||
for slot, item in pairs(items) do
|
||||
local key = table.concat({item.name, item.damage, item.nbtHash}, ":")
|
||||
if not res[key] then
|
||||
res[key] = {}
|
||||
end
|
||||
table.insert(res[key], {item = item, device = device[chest], lockedToThis = (self.nodes[chest].lock or {})[key] or false, slot = slot})
|
||||
end
|
||||
end
|
||||
return res
|
||||
end
|
||||
|
||||
-- defrags the storage system
|
||||
function Storage:defrag(throttle)
|
||||
local items = self:listProviders(throttle)
|
||||
local slotsSaved = 0
|
||||
|
||||
-- This will make sure the table is sorted in the following order:
|
||||
-- Unlocked stacks with less than maxCount items | Locked stacks with less than maxCount items | stacks with more than maxCount items
|
||||
-- This way the locked stacks will be filled first
|
||||
local function sortFunction(a, b)
|
||||
local preferenceA, preferenceB
|
||||
preferenceA = (a.item.count == a.item.maxCount and 3)
|
||||
or (a.lockedToThis and 2)
|
||||
or 1
|
||||
preferenceB = (b.item.count == b.item.maxCount and 3)
|
||||
or (b.lockedToThis and 2)
|
||||
or 1
|
||||
|
||||
if preferenceA < preferenceB then
|
||||
return true
|
||||
elseif preferenceB > preferenceA then
|
||||
return false
|
||||
else
|
||||
return a.item.count < b.item.count
|
||||
end
|
||||
end
|
||||
|
||||
for _, providers in pairs(items) do
|
||||
table.sort(providers, sortFunction)
|
||||
|
||||
-- We're done when we either compressed the stacks so far, that there's only one left (#providers == 1)
|
||||
-- Or when we've compressed so far, that the there's only one stack which has a lower count than the maxCount
|
||||
-- Because of the sorting, we know that this will be the stack in providers[1], so we check if providers[2] is at the maxCount
|
||||
while #providers > 1 and providers[2].item.count ~= providers[2].item.maxCount do
|
||||
local from = providers[1]
|
||||
local to
|
||||
|
||||
-- We're pushing to the highest stack which is still below the maxCount, this way as many slots as possible will be filled
|
||||
-- This loop is guarenteed to assign a value to "to", as the only cases where it wouldn't (#providers == 1 or no provider with less than maxCount)
|
||||
-- are ruled out by the condition of the outer while loop
|
||||
for i = 2, #providers do
|
||||
-- Give preference to locked chests
|
||||
if not (to and to.lockedToThis or false) and providers[i].lockedToThis then
|
||||
to = providers[i]
|
||||
elseif ((to and to.lockedToThis or false) == providers[i].lockedToThis) and providers[i].item.count < providers[i].item.maxCount then
|
||||
to = providers[i]
|
||||
elseif providers[i].item.count == providers[i].item.maxCount then
|
||||
-- As this slot is already at maxCount, all the remaining ones will also be due to sorting
|
||||
-- If any of the remaining providers is locked that doesn't matter. We wouldn't have been able to push there anyways
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
local toMove = math.min(to.item.maxCount - to.item.count, from.item.count)
|
||||
local s, m = pcall(function()
|
||||
from.device.pushItems(to.device.name, from.slot, toMove, to.slot)
|
||||
end)
|
||||
|
||||
if not s and m then
|
||||
_G._syslog(m)
|
||||
end
|
||||
|
||||
if s then
|
||||
to.item.count = to.item.count + toMove
|
||||
from.item.count = from.item.count - toMove
|
||||
else
|
||||
-- Do not try to send to the target again after it failed
|
||||
for i = 2, #providers do
|
||||
if to == providers[i] then
|
||||
table.remove(providers, i)
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if from.item.count <= 0 then
|
||||
table.remove(providers, 1)
|
||||
slotsSaved = slotsSaved + 1
|
||||
end
|
||||
|
||||
table.sort(providers, sortFunction)
|
||||
end
|
||||
end
|
||||
|
||||
return slotsSaved
|
||||
end
|
||||
|
||||
function Storage:updateCache(adapter, item, count)
|
||||
if not adapter.cache then
|
||||
adapter.dirty = true
|
||||
|
||||
@@ -33,6 +33,11 @@ local page = UI.Page {
|
||||
event = 'rescan',
|
||||
help = 'Rescan all inventories'
|
||||
},
|
||||
{
|
||||
text = 'Defragment storage',
|
||||
event = 'defrag',
|
||||
help = 'Defragments the storage'
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -250,6 +255,10 @@ function page:eventHandler(event)
|
||||
self.grid:draw()
|
||||
self:setFocus(self.statusBar.filter)
|
||||
|
||||
elseif event.type == 'defrag' then
|
||||
self:defrag()
|
||||
self:refresh(true)
|
||||
|
||||
elseif event.type == 'toggle_display' then
|
||||
displayMode = (displayMode + 1) % 2
|
||||
Util.merge(event.button, displayModes[displayMode])
|
||||
@@ -357,6 +366,15 @@ function page:refresh(force)
|
||||
self.throttle:disable()
|
||||
end
|
||||
|
||||
function page:defrag()
|
||||
local throttle = function() self.throttle:update() end
|
||||
|
||||
self.throttle:enable()
|
||||
local saved = context.storage:defrag(throttle)
|
||||
self.throttle:disable()
|
||||
self:notifyInfo(("Saved %d slots"):format(saved))
|
||||
end
|
||||
|
||||
function page:applyFilter()
|
||||
local function filterItems(t, filter)
|
||||
self.grid.sortColumn = Milo:getState('sortColumn') or 'count'
|
||||
|
||||
Reference in New Issue
Block a user
There might be cases where pushItems will generate an error (ie. if the user's ender chest is set to storage for some reason and they are offline). It would be best to wrap the pushItems in a pcall().