dfhack/plugins/lua/buildingplan.lua

286 lines
10 KiB
Lua

local _ENV = mkmodule('plugins.buildingplan')
--[[
Native functions:
* void setSetting(string name, boolean value)
* bool isPlanModeEnabled(df::building_type type, int16_t subtype, int32_t custom)
* bool isPlannableBuilding(df::building_type type, int16_t subtype, int32_t custom)
* bool isPlannedBuilding(df::building *bld)
* void addPlannedBuilding(df::building *bld)
* void doCycle()
* void scheduleCycle()
--]]
local argparse = require('argparse')
require('dfhack.buildings')
local function process_args(opts, args)
if args[1] == 'help' then
opts.help = true
return
end
return argparse.processArgsGetopt(args, {
{'h', 'help', handler=function() opts.help = true end},
})
end
function parse_commandline(...)
local args, opts = {...}, {}
local positionals = process_args(opts, args)
if opts.help then
return false
end
return true
end
function get_num_filters(btype, subtype, custom)
local filters = dfhack.buildings.getFiltersByType(
{}, btype, subtype, custom)
if filters then return #filters end
return 0
end
local dialogs = require('gui.dialogs')
local guidm = require('gui.dwarfmode')
local function to_title_case(str)
str = str:gsub('(%a)([%w_]*)',
function (first, rest) return first:upper()..rest:lower() end)
str = str:gsub('_', ' ')
return str
end
local function get_filter(btype, subtype, custom, reverse_idx)
local filters = dfhack.buildings.getFiltersByType(
{}, btype, subtype, custom)
if not filters or reverse_idx < 0 or reverse_idx >= #filters then
error(string.format('invalid index: %d', reverse_idx))
end
return filters[#filters-reverse_idx]
end
-- returns a reasonable label for the item based on the qualities of the filter
-- does not need the core suspended
-- reverse_idx is 0-based and is expected to be counted from the *last* filter
function get_item_label(btype, subtype, custom, reverse_idx)
local filter = get_filter(btype, subtype, custom, reverse_idx)
if filter.has_tool_use then
return to_title_case(df.tool_uses[filter.has_tool_use])
end
if filter.item_type then
return to_title_case(df.item_type[filter.item_type])
end
if filter.flags2 and filter.flags2.building_material then
if filter.flags2.fire_safe then
return "Fire-safe building material";
end
if filter.flags2.magma_safe then
return "Magma-safe building material";
end
return "Generic building material";
end
if filter.vector_id then
return to_title_case(df.job_item_vector_id[filter.vector_id])
end
return "Unknown";
end
-- returns whether the items matched by the specified filter can have a quality
-- rating. This also conveniently indicates whether an item can be decorated.
-- does not need the core suspended
-- reverse_idx is 0-based and is expected to be counted from the *last* filter
function item_can_be_improved(btype, subtype, custom, reverse_idx)
local filter = get_filter(btype, subtype, custom, reverse_idx)
if filter.flags2 and filter.flags2.building_material then
return false;
end
return filter.item_type ~= df.item_type.WOOD and
filter.item_type ~= df.item_type.BLOCKS and
filter.item_type ~= df.item_type.BAR and
filter.item_type ~= df.item_type.BOULDER
end
-- needs the core suspended
-- returns a vector of constructed buildings (usually of size 1, but potentially
-- more for constructions)
function construct_buildings_from_ui_state()
local uibs = df.global.buildreq
local world = df.global.world
local direction = world.selected_direction
local _, width, height = dfhack.buildings.getCorrectSize(
world.building_width, world.building_height, uibs.building_type,
uibs.building_subtype, uibs.custom_type, direction)
-- the cursor is at the center of the building; we need the upper-left
-- corner of the building
local pos = guidm.getCursorPos()
pos.x = pos.x - math.floor(width/2)
pos.y = pos.y - math.floor(height/2)
local min_x, max_x = pos.x, pos.x
local min_y, max_y = pos.y, pos.y
if width == 1 and height == 1 and
(world.building_width > 1 or world.building_height > 1) then
min_x = math.ceil(pos.x - world.building_width/2)
max_x = min_x + world.building_width - 1
min_y = math.ceil(pos.y - world.building_height/2)
max_y = min_y + world.building_height - 1
end
local blds = {}
for y=min_y,max_y do for x=min_x,max_x do
local bld, err = dfhack.buildings.constructBuilding{
type=uibs.building_type, subtype=uibs.building_subtype,
custom=uibs.custom_type, pos=xyz2pos(x, y, pos.z),
width=width, height=height, direction=direction}
if err then
for _,b in ipairs(blds) do
dfhack.buildings.deconstruct(b)
end
error(err)
end
-- assign fields for the types that need them. we can't pass them all in
-- to the call to constructBuilding since attempting to assign unrelated
-- fields to building types that don't support them causes errors.
for k,v in pairs(bld) do
if k == 'friction' then bld.friction = uibs.friction end
if k == 'use_dump' then bld.use_dump = uibs.use_dump end
if k == 'dump_x_shift' then bld.dump_x_shift = uibs.dump_x_shift end
if k == 'dump_y_shift' then bld.dump_y_shift = uibs.dump_y_shift end
if k == 'speed' then bld.speed = uibs.speed end
end
table.insert(blds, bld)
end end
return blds
end
--
-- GlobalSettings dialog
--
local GlobalSettings = defclass(GlobalSettings, dialogs.MessageBox)
GlobalSettings.focus_path = 'buildingplan_globalsettings'
GlobalSettings.ATTRS{
settings = {}
}
function GlobalSettings:onDismiss()
for k,v in pairs(self.settings) do
-- call back into C++ to save changes
setSetting(k, v)
end
end
-- does not need the core suspended.
function show_global_settings_dialog(settings)
GlobalSettings{
frame_title="Buildingplan Global Settings",
settings=settings,
}:show()
end
function GlobalSettings:toggle_setting(name)
self.settings[name] = not self.settings[name]
end
function GlobalSettings:get_setting_string(name)
if self.settings[name] then return 'On' end
return 'Off'
end
function GlobalSettings:get_setting_pen(name)
if self.settings[name] then return COLOR_LIGHTGREEN end
return COLOR_LIGHTRED
end
function GlobalSettings:is_setting_enabled(name)
return self.settings[name]
end
function GlobalSettings:make_setting_label_token(text, key, name, width)
return {text=text, key=key, key_sep=': ', key_pen=COLOR_LIGHTGREEN,
on_activate=self:callback('toggle_setting', name), width=width}
end
function GlobalSettings:make_setting_value_token(name)
return {text=self:callback('get_setting_string', name),
enabled=self:callback('is_setting_enabled', name),
pen=self:callback('get_setting_pen', name),
dpen=COLOR_GRAY}
end
-- mockup:
--[[
Buildingplan Global Settings
e: Enable all: Off
Enables buildingplan for all building types. Use this to avoid having to
manually enable buildingplan for each building type that you want to plan.
Note that DFHack quickfort will use buildingplan to manage buildings
regardless of whether buildingplan is "enabled" for the building type.
Allowed types for generic, fire-safe, and magma-safe building material:
b: Blocks: On
s: Boulders: On
w: Wood: On
r: Bars: Off
Changes to these settings will be applied to newly-planned buildings.
A: Apply building material filter settings to existing planned buildings
Use this if your planned buildings can't be completed because the settings
above were too restrictive when the buildings were originally planned.
M: Edit list of materials to avoid
potash
pearlash
ash
coal
Buildingplan will avoid using these material types when a planned building's
material filter is set to 'any'. They can stil be matched when they are
explicitly allowed by a planned building's material filter. Changes to this
list take effect for existing buildings immediately.
g: Allow bags: Off
This allows bags to be placed where a 'coffer' is planned.
f: Legacy Quickfort Mode: Off
Compatibility mode for the legacy Python-based Quickfort application. This
setting is not needed for DFHack quickfort.
--]]
function GlobalSettings:init()
self.subviews.label:setText{
self:make_setting_label_token('Enable all', 'CUSTOM_E', 'all_enabled', 12),
self:make_setting_value_token('all_enabled'), '\n',
' Enables buildingplan for all building types. Use this to avoid having\n',
' to manually enable buildingplan for each building type that you want\n',
' to plan. Note that DFHack quickfort will use buildingplan to manage\n',
' buildings regardless of whether buildingplan is "enabled" for the\n',
' building type.\n',
'\n',
'Allowed types for generic, fire-safe, and magma-safe building material:\n',
self:make_setting_label_token('Blocks', 'CUSTOM_B', 'blocks', 10),
self:make_setting_value_token('blocks'), '\n',
self:make_setting_label_token('Boulders', 'CUSTOM_S', 'boulders', 10),
self:make_setting_value_token('boulders'), '\n',
self:make_setting_label_token('Wood', 'CUSTOM_W', 'logs', 10),
self:make_setting_value_token('logs'), '\n',
self:make_setting_label_token('Bars', 'CUSTOM_R', 'bars', 10),
self:make_setting_value_token('bars'), '\n',
' Changes to these settings will be applied to newly-planned buildings.\n',
' If no types are enabled above, then any building material is allowed.\n',
'\n',
self:make_setting_label_token('Legacy Quickfort Mode', 'CUSTOM_F',
'quickfort_mode', 23),
self:make_setting_value_token('quickfort_mode'), '\n',
' Compatibility mode for the legacy Python-based Quickfort application.\n',
' This setting is not needed for DFHack quickfort.'
}
end
return _ENV