Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions caravan.lua
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ OVERLAY_WIDGETS = {
movegoods_hider=movegoods.MoveGoodsHiderOverlay,
assigntrade=movegoods.AssignTradeOverlay,
displayitemselector=pedestal.PedestalOverlay,
autobarter=trade.AutoBarterOverlay,
}

INTERESTING_FLAGS = {
Expand Down
8 changes: 8 additions & 0 deletions docs/caravan.rst
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,13 @@ are trading with. Clicking on the badge will show a list of problematic items,
and you can click the button on the dialog to deselect all the problematic
items in your trade list.

**caravan.autobarter**

This overlay provides "Barter (expensive)" and "Barter (cheap)" buttons on the trade
screen. These buttons automatically select fortress goods to trade that total slightly
more than the value of the merchant goods you have selected. You can choose whether to
fill the quota with your most expensive items or your cheapest items.

Trade agreements
````````````````

Expand All @@ -141,6 +148,7 @@ Trade agreements
This adds a small panel with some useful shortcuts:

* ``Ctrl-a`` for selecting all/none in the currently shown category.
* ``Shift-Ctrl-a`` for selecting all/none across all item categories globally.
* ``Ctrl-m`` for selecting items with specific base material price (only
enabled for item categories where this matters, like gems and leather).

Expand Down
193 changes: 193 additions & 0 deletions internal/caravan/trade.lua
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@ handle_shift_click_on_render = handle_shift_click_on_render or false

local trade = df.global.game.main_interface.trade

-- Auto Barter: configurable profit margin for the merchant.
-- 1.25 = offer 25% more value than the merchant's goods you want.
-- Increase if merchants keep rejecting. Decrease as your broker skill improves.
local TARGET_MARGIN = 1.25

-- -------------------
-- Trade
--
Expand Down Expand Up @@ -684,6 +689,163 @@ local function getSavedGoodflag(saved_state, k)
return saved_state[k+1]
end

-- -------------------
-- Auto Barter
--

local function get_selected_merchant_value()
local total = 0
local goodflags = trade.goodflag[0]
local in_selected_container = false
for item_idx, item in ipairs(trade.good[0]) do
local goodflag = goodflags[item_idx]
if not goodflag.contained then
in_selected_container = goodflag.selected
if goodflag.selected then
total = total + common.get_perceived_value(item, trade.mer)
end
else
if not in_selected_container and goodflag.selected then
total = total + common.get_perceived_value(item, trade.mer)
end
end
end
return total
end

local function build_fort_selectable_units()
local banned_items = common.get_banned_items()
local risky_items = common.get_risky_items(banned_items)
local units = {}
local current_container = nil

for item_idx, item in ipairs(trade.good[1]) do
local goodflag = trade.goodflag[1][item_idx]

if goodflag.contained then
-- this item is inside a container; attach it to the current container unit
if current_container then
table.insert(current_container.contained_indices, item_idx)
-- check mandate on contained item
local is_banned, _ = common.scan_banned(item, risky_items)
if is_banned then
current_container.has_banned = true
end
end
else
-- flush previous container
current_container = nil

local is_banned, _ = common.scan_banned(item, risky_items)
local is_container = df.item_binst:is_instance(item)

local unit = {
item_idx = item_idx,
value = common.get_perceived_value(item, trade.mer),
is_container = is_container,
contained_indices = {},
has_banned = is_banned,
}

if is_container then
current_container = unit
end

table.insert(units, unit)
end
end

-- filter out banned units
local filtered = {}
for _, unit in ipairs(units) do
if not unit.has_banned then
table.insert(filtered, unit)
end
end
return filtered
end

local function auto_barter(expensive_first)
local merchant_value = get_selected_merchant_value()
if merchant_value <= 0 then
dfhack.printerr('Auto Barter: Select merchant goods first!')
return
end

local target_value = math.ceil(merchant_value * TARGET_MARGIN)

-- deselect all fortress goods
for item_idx in ipairs(trade.good[1]) do
trade.goodflag[1][item_idx].selected = false
end

-- build and sort selectable units
local units = build_fort_selectable_units()
if expensive_first then
table.sort(units, function(a, b) return a.value > b.value end)
else
table.sort(units, function(a, b) return a.value < b.value end)
end

-- knapsack greedy selection with overshoot protection
local running_total = 0
local selected_units = {}

for _, unit in ipairs(units) do
if running_total >= target_value then break end

local remaining = target_value - running_total
-- overshoot protection: if this unit's value is more than 2x what we
-- still need AND there are cheaper options ahead, skip it
if unit.value > remaining * 2 and remaining > 0 then
-- but if we have nothing selected yet, we must take something
if #selected_units > 0 then
goto continue
end
end

running_total = running_total + unit.value
table.insert(selected_units, unit)

::continue::
end

-- if we overshot badly in the first pass, do a second pass to find
-- tighter fits from remaining items
if running_total < target_value then
for _, unit in ipairs(units) do
if running_total >= target_value then break end
-- check if already selected
local dominated = false
for _, sel in ipairs(selected_units) do
if sel.item_idx == unit.item_idx then
dominated = true
break
end
end
if not dominated then
running_total = running_total + unit.value
table.insert(selected_units, unit)
end
end
end

-- apply selections to trade.goodflag
for _, unit in ipairs(selected_units) do
trade.goodflag[1][unit.item_idx].selected = true
-- if it's a container, also select all contained items
for _, cidx in ipairs(unit.contained_indices) do
trade.goodflag[1][cidx].selected = true
end
end

local value_str = dfhack.formatInt(running_total)
local target_str = dfhack.formatInt(target_value)
local merchant_str = dfhack.formatInt(merchant_value)
print(('Auto Barter: Merchant goods=%s, Target=%s (x%.2f), Offering=%s (%d items)'):format(
merchant_str, target_str, TARGET_MARGIN, value_str, #selected_units))
end

TradeOverlay = defclass(TradeOverlay, overlay.OverlayWidget)
TradeOverlay.ATTRS{
desc='Adds convenience functions for working with bins to the trade screen.',
Expand Down Expand Up @@ -801,6 +963,37 @@ function TradeBannerOverlay:onInput(keys)
end
end

-- -------------------
-- AutoBarterOverlay
--

AutoBarterOverlay = defclass(AutoBarterOverlay, overlay.OverlayWidget)
AutoBarterOverlay.ATTRS{
desc='Adds auto barter buttons to the trade screen.',
default_pos={x=-31,y=-5},
default_enabled=true,
viewscreens='dwarfmode/Trade/Default',
frame={w=25, h=2},
frame_background=gui.CLEAR_PEN,
}

function AutoBarterOverlay:init()
self:addviews{
widgets.TextButton{
frame={t=0, l=0},
label='Barter (expensive)',
key='CUSTOM_CTRL_E',
on_activate=function() auto_barter(true) end,
},
widgets.TextButton{
frame={t=1, l=0},
label='Barter (cheap)',
key='CUSTOM_CTRL_W',
on_activate=function() auto_barter(false) end,
},
}
end

-- -------------------
-- Ethics
--
Expand Down
35 changes: 34 additions & 1 deletion internal/caravan/tradeagreement.lua
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ TradeAgreementOverlay.ATTRS{
default_pos={x=45, y=-6},
default_enabled=true,
viewscreens='dwarfmode/Diplomacy/Requests',
frame={w=25, h=4},
frame={w=35, h=5},
frame_style=gui.MEDIUM_FRAME,
frame_background=gui.CLEAR_PEN,
}
Expand Down Expand Up @@ -71,6 +71,31 @@ local function diplomacy_toggle_cat()
end
end

local function diplomacy_toggle_all_cats()
local target_val = 4
local all_selected = true
for _, cat in ipairs(diplomacy.taking_requests_tablist) do
local priority = diplomacy.environment.dipev.sell_requests.priority[cat]
for i in ipairs(priority) do
if priority[i] ~= 4 then
all_selected = false
break
end
end
if not all_selected then break end
end
if all_selected then
target_val = 0
end

for _, cat in ipairs(diplomacy.taking_requests_tablist) do
local priority = diplomacy.environment.dipev.sell_requests.priority[cat]
for i in ipairs(priority) do
priority[i] = target_val
end
end
end

local function select_by_value(prices, val)
local priority = get_cur_priority_list()
for i in ipairs(priority) do
Expand All @@ -92,6 +117,14 @@ function TradeAgreementOverlay:init()
self:addviews{
widgets.HotkeyLabel{
frame={t=1, l=0},
label='Select globally',
key='CUSTOM_SHIFT_A',
on_activate=diplomacy_toggle_all_cats,
},
}
self:addviews{
widgets.HotkeyLabel{
frame={t=2, l=0},
label='Select by value',
key='CUSTOM_CTRL_M',
on_activate=self:callback('select_by_value'),
Expand Down