381 lines
13 KiB
Lua

local this = {}
local util = scripts.util
local visuals = scripts.visuals
local metatables = scripts.metatables
local config = require("config")
local helpers = scripts.helpers
local _ = helpers.on
function this.on_tick(event) -- handles distribution events
local distrEvents = global.distrEvents
if distrEvents[event.tick] then
for player_index, cache in pairs(distrEvents[event.tick]) do
local player = _(game.players[player_index])
if player:is("valid player") then
if player:setting("distributionMode") == "distribute" then
this.distributeItems(player, cache)
else
this.balanceItems(player, cache)
end
end
this.resetCache(cache)
end
distrEvents[event.tick] = nil
end
end
function this.distributeItems(player, cache)
local useFuelLimit = player:setting("dragUseFuelLimit")
local useAmmoLimit = player:setting("dragUseAmmoLimit")
local takeFromInv = player:setting("takeFromInventory")
local takeFromCar = player:setting("takeFromCar")
local replaceItems = player:setting("replaceItems")
local item = cache.item
local totalItems = player:itemcount(item, takeFromInv, takeFromCar)
if cache.half then totalItems = math.ceil(totalItems / 2) end
util.distribute(cache.entities, totalItems, function(entity, amount)
local itemsInserted = 0
local color
if amount > 0 then
local takenFromPlayer = player:removeItems(item, amount, takeFromInv, takeFromCar, false)
if takenFromPlayer < amount then color = config.colors.insufficientItems end
if takenFromPlayer > 0 then
itemsInserted = entity:customInsert(player, item, takenFromPlayer, takeFromCar, false, replaceItems, useFuelLimit, useAmmoLimit, false, {
-- if modules are recipe ingredients, dont put into module slots
-- modules = not entity:is("crafting machine") or not _(entity.get_recipe()):hasIngredient(item),
output = false,
})
local failedToInsert = takenFromPlayer - itemsInserted
if failedToInsert > 0 then
player:returnItems(item, failedToInsert, takeFromCar, false)
color = config.colors.targetFull
end
end
else
color = config.colors.insufficientItems
end
-- feedback
entity:spawnDistributionText(item, itemsInserted, 0, color)
-- player.play_sound{ path = "utility/inventory_move" }
end)
end
function this.balanceItems(player, cache)
local useFuelLimit = player:setting("dragUseFuelLimit")
local useAmmoLimit = player:setting("dragUseAmmoLimit")
local takeFromInv = player:setting("takeFromInventory")
local takeFromCar = player:setting("takeFromCar")
local replaceItems = player:setting("replaceItems")
local item = cache.item
local entitiesToProcess = metatables.new("entityAsIndex")
local itemCounts = metatables.new("entityAsIndex")
local totalItems = player:itemcount(item, takeFromInv, takeFromCar)
if cache.half then totalItems = math.ceil(totalItems / 2) end
if totalItems > 0 then
totalItems = player:removeItems(item, totalItems, takeFromInv, takeFromCar, false)
end
-- collect all items from all entities
_(cache.entities):where("valid", function(entity)
local count = _(entity):itemcount(item)
local removed = 0
if count > 0 then
removed = entity.remove_item{ name = item, count = count }
totalItems = totalItems + removed
end
-- save entities in new list
entitiesToProcess[entity] = entity
itemCounts[entity] = {
original = count,
remaining = count - removed, -- amount above balanced level (unable to take out)
current = count - removed,
}
end)
-- distribute collected items evenly
local i = 0
while totalItems > 0 and #entitiesToProcess > 0 do -- retry if some containers full
if i == 1000 then dlog("WARNING: Balance item loop did not finish!"); break end -- safeguard
i = i + 1
util.distribute(entitiesToProcess, totalItems, function(entity, amount)
local itemCount = itemCounts[entity]
amount = amount - itemCount.remaining
if amount > 0 then
local itemsInserted = entity:customInsert(player, item, amount, takeFromCar, false, replaceItems, useFuelLimit, useAmmoLimit, false, {
-- if modules are recipe ingredients, dont put into module slots
-- modules = not entity:is("crafting machine") or not _(entity.get_recipe()):hasIngredient(item),
output = false,
})
itemCount.current = itemCount.current + itemsInserted
totalItems = totalItems - itemsInserted
local failedToInsert = amount - itemsInserted
if failedToInsert > 0 then
entity:spawnDistributionText(item, itemCount.current - itemCount.original, 0, config.colors.targetFull)
entitiesToProcess[entity] = nil -- set nil while iterating bad?
return
end
amount = 0
end
itemCount.remaining = -amount -- update remaining item count (amount above balanced level)
-- add entity to new list?
end)
end
_(entitiesToProcess):each(function(entity)
local itemCount = itemCounts[entity]
local amount = itemCount.current - itemCount.original
_(entity):spawnDistributionText(item, amount, 0, (itemCount.current == 0) and config.colors.insufficientItems
or config.colors.default)
end)
if totalItems > 0 then
player:returnItems(item, totalItems, takeFromCar, false)
end
end
function this.on_fast_entity_transfer_hook(event)
local index = event.player_index
local player = _(game.players[index]); if player:isnot("valid player") then return end
local cache = _(global.cache[index])
cache.half = false
end
function this.on_fast_entity_split_hook(event)
local index = event.player_index
local player = _(game.players[index]); if player:isnot("valid player") then return end
local cache = _(global.cache[index])
cache.half = true
end
function this.on_selected_entity_changed(event)
local index = event.player_index
local player = _(game.players[index]); if player:isnot("valid player") or not player:setting("enableDragDistribute") then return end
local cursor_stack = _(player.cursor_stack); if cursor_stack:isnot("valid stack") then return end
local cache = _(global.cache[index])
local selected = _(player.selected) ; if selected:isnot("valid") or selected:isIgnored(player) then return end
-- if not selected.can_insert{ name = cursor_stack.name, count = 1 } then return end
cache.selectedEvent = {
tick = event.tick,
item = cursor_stack.name,
itemCount = selected:itemcount(cursor_stack.name),
cursorStackCount = cursor_stack.count,
}
end
function this.on_player_fast_transferred(event)
local index = event.player_index
local player = _(game.players[index]); if player:isnot("valid player") or not player:setting("enableDragDistribute") then return end
local cache = _(global.cache[index])
local selected = _(player.selected) ; if selected:isnot("valid") or selected:isIgnored(player) then return end
if cache.selectedEvent and cache.selectedEvent.tick == event.tick and event.entity == selected:toPlain() then
if event.from_player then
-- distribute...
if cache.selectedEvent.item then
cache:set{
item = cache.selectedEvent.item,
itemCount = selected:itemcount(cache.selectedEvent.item) - cache.selectedEvent.itemCount,
cursorStackCount = cache.selectedEvent.cursorStackCount,
}
this.onStackTransferred(selected, player, cache) -- handle stack transfer
end
else
-- take...
end
end
end
function this.onStackTransferred(entity, player, cache) -- handle vanilla drag stack transfer
local takeFromInv = player:setting("takeFromInventory")
local takeFromCar = player:setting("takeFromCar")
local distributionMode = player:setting("distributionMode")
local item = cache.item
local first = #cache.entities > 0 and util.epairs(cache.entities)() or nil
if not _(entity):isIgnored(player) and this.isEntityEligible(entity, item) and
(first == nil or ((first.type == "car" or first.type == "spider-vehicle") and (entity.type == "car" or entity.type == "spider-vehicle"))
or ((first.type ~= "car" and first.type ~= "spider-vehicle") and (entity.type ~= "car" and entity.type ~= "spider-vehicle"))) then
local distrEvents = global.distrEvents -- register new distribution event
if cache.applyTick and distrEvents[cache.applyTick] then distrEvents[cache.applyTick][player.index] = nil end
-- wait before applying distribution (seconds defined in mod config)
cache.applyTick = game.tick + math.max(math.ceil(60 * _(player):setting("distributionDelay")), 1)
distrEvents[cache.applyTick] = distrEvents[cache.applyTick] or {}
distrEvents[cache.applyTick][player.index] = cache
if not cache.entities[entity] then
cache.markers[entity] = entity:mark(player, item)
cache.entities[entity] = entity
end
if cache.itemCount == 0 then
player.play_sound{ path = "utility/inventory_move" }
end
end
-- give back transferred items
local collected = 0
local cursor_stack = player.cursor_stack
if cache.itemCount > 0 then
collected = entity.remove_item{ name = item, count = cache.itemCount }
end
if cursor_stack.valid_for_read and cursor_stack.name ~= item then
-- other items in cursor
player:inventory().insert{ name = item, count = collected }
else -- same items
-- collect cursor and transferred items temporarily
if cursor_stack.valid_for_read then
collected = collected + cursor_stack.count
end
-- fill cursor to previous amount
if collected < cache.cursorStackCount then
collected = collected + player:inventory().remove{ name = item, count = cache.cursorStackCount - collected }
end
if collected > 0 then
if collected < cache.cursorStackCount then
cursor_stack.set_stack{ name = item, count = collected }
else
cursor_stack.set_stack{ name = item, count = cache.cursorStackCount }
collected = collected - cache.cursorStackCount
if collected > 0 then
player:inventory().insert{ name = item, count = collected }
end
end
end
end
---- visuals ----
local totalItems = player:itemcount(item, takeFromInv, takeFromCar)
if cache.half then totalItems = math.ceil(totalItems / 2) end
if distributionMode == "balance" then
_(cache.entities):where("valid", function(entity)
totalItems = totalItems + _(entity):itemcount(item)
end)
end
util.distribute(cache.entities, totalItems, function(entity, amount)
-- if distributionMode == "balance" then
-- local count = _(entity):itemcount(item)
-- if count > amount then
-- visuals.update(cache.markers[entity], item, count, config.colors.targetFull)
-- return
-- end
-- end
visuals.update(cache.markers[entity], item, amount)
end)
entity:destroyTransferText()
end
function this.isEntityEligible(entity, item)
local prototype = game.item_prototypes[item]
entity = _(entity)
if entity.can_insert(item) then
return true
elseif entity.burner and entity.burner.fuel_categories[prototype.fuel_category] then
return true
elseif entity:is("crafting machine") and entity:recipe():hasIngredient(item) then
return true
elseif entity.type == "furnace" and not entity.get_recipe() and entity:canSmelt(item) then
return true
elseif entity.type == "rocket-silo" then
return true
elseif entity.type == "lab" and entity:inventory("lab_input").can_insert(item) then
return true
elseif (entity.type == "ammo-turret" or entity.type == "artillery-turret" or entity.type == "artillery-wagon") and entity:supportsAmmo(prototype) then
return true
elseif entity.type == "roboport" and (prototype.type == "repair-tool" or (prototype.place_result and prototype.place_result.max_payload_size ~= nil)) then
return true
elseif entity.prototype.module_inventory_size and entity.prototype.module_inventory_size > 0 and prototype.type == "module" then
return true
elseif entity:inventory("main") then
return true
end
return false
end
function this.resetCache(cache)
cache.item = nil
cache.half = false
cache.entities = metatables.new("entityAsIndex")
visuals.unmark(cache)
end
function this.on_pre_player_mined_item(event) -- remove mined/dead entities from cache
local entity = event.entity
for __,cache in pairs(global.cache) do
if cache.entities[entity] then
_(cache.markers[entity]):unmark() -- remove markers
cache.markers[entity] = nil
cache.entities[entity] = nil
end
end
end
this.on_robot_pre_mined = this.on_pre_player_mined_item
this.on_entity_died = this.on_pre_player_mined_item
function this.script_raised_destroy(event)
event = event or {}
event.entity = event.entity or event.destroyed_entity or event.destroyedEntity or event.target or nil
if _(event.entity):is("valid") then this.on_pre_player_mined_item(event) end
end
function this.on_player_died(event) -- resets distribution cache and events for that player
local cache = global.cache[event.player_index]
if cache then
this.resetCache(cache)
local distrEvents = global.distrEvents -- remove distribution event
if cache.applyTick and distrEvents[cache.applyTick] then
distrEvents[cache.applyTick][event.player_index] = nil
cache.applyTick = nil
end
end
end
this.on_player_left_game = this.on_player_died
return this